diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..8d7c66c --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,25 @@ +# Explosion der Spiele – Design-Spec (kanonisch) + +Quelle: mündlicher/diktierter Gesamtplan. Scaffold folgt diesem Flow. + +## Kamera / Grafik +- 3D-Welt, alles als Billboard-Planes (Mario-Bäume), Third-Person-Follow-Kamera + +## Flow +1. **Title** – verpixelt Mario/Sonic/Burger-Regen, verstörter Mensch + Blase, „Explosion der Spiele / Pay2Win (um reiche Leute anzulocken, hihihihi)“, extrem laute Musik (nicht leiser), Burger-Cham alle 2 Min, First-Run Sprachwahl DE/EN/BY +2. **Intro** – Tisch + Konsole, epischer Sprecher, Explosion, „sonst kommt der Hongkong“ +3. **Village** – kaputtes AC-Dorf, fliehende Dorfbewohner („Hongkong!“), Raum mit Tisch, Knopf unter Tisch → Schuhmacher Reif +4. **Shoe Boss (Reif)** – Mensch + Stock + Schuh; Sieg → Tonne Tiramisu → 10 Min „zu fett“ + Shorts-Stimulation (Subway Surf / satisfying) +5. **Meadow** – Eisbär vs Schlangenmähdrescher; Schlangen ins Inventar → Massage-Studio; Zahn-Belohnung; Glitzos (4 Hits / 4 Hits tot; Zahn → 2 Hits; nur auf Wiese) +6. **Massage Studio** – Kuh-Masseur; Secret: trinken → melkt sich → Milch → Skip zum Endboss +7. **Catscene** – „keine Open World“; Skip-Versuch → Reset von vorne (Wortwitz cat/Katze) +8. **Farm Path** – enger Weg, Gabelung; links Hof; rechts dunkelgrauer Magier (zurückwerfen / Wut → Planet weg → Mond) +9. **Farm** – Bayer immer BY; Schweine/Kühe/Hühner+Urhahn; Lava-Chicken; Paywall 1,99 / Dresslerpunkte (Spaß-weiter); Schein der Uralten +10. **Dressler** – Riesenapfel mit Augen, Mähdrescher hijacken; drop zertresselten Apfel → essen → Teleport Endboss +11. **Moon** – Käseverkäufer/Katapulte, Mondschwert, Sonnenbrille gratis, Fake-Bowser Hammer-Minispiel, Hochzeitsei/Whiff +12. **Final** – alle: Hongkong war Fake → Glitschkönig; Banane + Wartchen; Pixelhaufen + Quirxels; Split; „ich werde wiederkommen“; Eisbär-Zahnarzt-Guilt +13. **End** – AC-Dorf, Hausbau, wütende Dorfbewohner; Quest Zahnionär (Zahnarzt überfallen) +14. **Death** – bunt blinkend, Totenkopf alle 10s, Todesursachen, Wiederbeleben Ja/Nein (Nein = trotzdem revive, Ja = Beenden – absurde Satire) + +## Pay-to-Win +- Dresslerpunkt = 1€ Mock; Shop in Optionen; Paywall-Texte mit „Spaß, du darfst weiter“ diff --git a/README.md b/README.md index 7a77b94..c7382e7 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,26 @@ Pay2Win – um reiche Leute anzulocken. Absichtlich chaotisches Meta-Comedy-Acti | Aktion | Taste | |--------|--------| -| Bewegen | WASD / Pfeile | +| Bewegen | WASD / Pfeile (relativ zur Kamera) | +| Umschauen | Maus | | Interagieren | E / Leertaste | | Angriff | Linksklick / J | | Pause (Shop/Inventar/Settings) | Esc | | Cutscene skippen | Enter | +Third-Person-Kamera folgt dem Spieler (`SpringArm3D`). Billboards nutzen Y-Billboard (Mario-Baum-Stil). + Im Inventar (Pause): Item doppelklicken/aktivieren — **Milch** skippt zum Endboss. ## Scaffold-Inhalt -- 12 Szenen verdrahtet (Title → Intro → Village → … → Final → End + Death) -- Autoloads: `GameState`, `Inventory`, `Achievements`, `Settings`, `Locale` (DE/EN/BY), `Shop` (Mock), `AudioDirector`, `UIRoot` -- Billboard-Prefab (`PlaneMesh` + `billboard_mode`) -- Pay-to-Win-Shop ohne echte Zahlungen (`1€ = 1 Dresslerpunkt` Mock-Button) -- Musik-Lautstärke absichtlich gesperrt (bleibt laut) -- Burger-Cham-Event alle 120s auf dem Title Screen -- Save unter `user://save.json` +Kanonischer Plan: **[DESIGN.md](DESIGN.md)** + +- Story-Flow inkl. Catscene + Dressler-Boss; Magier-Branch → Mond +- Third-Person-Kamera, Billboard-Y-Planes +- First-Run-Sprache DE/EN/BY, Musik gesperrt laut, Burger-Cham +- Milch-Skip, Tiramisu-Fett, Glitzos, Bowser-Hammer, Death Ja/Nein-Satire +- Save: `user://save.json` ## Regenerieren von Platzhaltern diff --git a/data/items.json b/data/items.json index 9c81345..8c1689e 100644 --- a/data/items.json +++ b/data/items.json @@ -1,13 +1,16 @@ [ {"id": "tooth", "name_key": "item_tooth", "stackable": true}, - {"id": "snake", "name_key": "item_snake", "stackable": false}, + {"id": "snake", "name_key": "item_snake", "stackable": true}, {"id": "milk", "name_key": "item_milk", "stackable": true, "usable": true}, - {"id": "banana", "name_key": "item_banana", "stackable": true}, - {"id": "wartchen", "name_key": "item_wartchen", "stackable": true}, + {"id": "banana", "name_key": "item_banana", "stackable": true, "usable": true}, + {"id": "wartchen", "name_key": "item_wartchen", "stackable": false}, {"id": "sunglasses", "name_key": "item_sunglasses", "stackable": false}, {"id": "death_ticket", "name_key": "item_death_ticket", "stackable": true, "usable": true}, {"id": "moon_sword", "name_key": "item_moon_sword", "stackable": false}, {"id": "chicken", "name_key": "item_chicken", "stackable": true}, {"id": "cheese", "name_key": "item_cheese", "stackable": true}, - {"id": "tiramisu", "name_key": "item_tiramisu", "stackable": true, "usable": true} + {"id": "tiramisu", "name_key": "item_tiramisu", "stackable": true, "usable": true}, + {"id": "dressler_apple", "name_key": "item_dressler_apple", "stackable": false, "usable": true}, + {"id": "wedding_egg", "name_key": "item_wedding_egg", "stackable": false, "usable": true}, + {"id": "lava_chicken", "name_key": "item_lava_chicken", "stackable": true} ] diff --git a/data/locales/by.json b/data/locales/by.json index 3b8b403..f671280 100644 --- a/data/locales/by.json +++ b/data/locales/by.json @@ -20,32 +20,32 @@ "lang_voice_de": "Am deutschen Wesen soll die Welt genesen", "lang_voice_en": "I drink my garden in the cinema", "lang_voice_by": "Schulz", - "dialog_reif": "Wennst no ned reif bist, muaß i da an Schuah schenken!", + "dialog_reif": "Wenn du noch nicht reif bist, muss ich dir einen Schuh schenken!", "dialog_snake": "I bin ganz verspannt, oi tut des weh", "dialog_bear": "Hongkong hot den Rasenmäher zu Schlangen gmochd", "dialog_hongkong_npc": "Hongkong, Hongkong is er!", - "dialog_cow": "Massage: Schlang / Spieler / Quadrat-Emoji", + "dialog_cow": "Masseur-Kuh: Massiere die Schlange jetzt – oder soll ich erst viel trinken?", "dialog_farmer": "Servus! De Schweine brauchn Quests und der Urhahn pickt.", "dialog_mage": "Rechts gehts zum Hof. Links is Fake-Open-World.", - "dialog_cheese": "Der Mond is aus Kas. Willst a Sonnenbrille?", - "dialog_bowser": "I halt a Hendl gfangn! (Fake Bowser)", - "dialog_hongkong_boss": "I kimm wieder!", - "dialog_bear_end": "Guad dassd unsere Welt grettet host. Aber Zahnarzt kostet immer no.", + "dialog_cheese": "Verschimmelter Käse! Nicht den Mund treffen lassen – stech den KÄSE, nicht den Verkäufer!", + "dialog_bowser": "Fake Bowser hält das Huhn! Rate den richtigen Hammer – sonst frisst er dich.", + "dialog_hongkong_boss": "Ich werde wiederkommen!", + "dialog_bear_end": "Gut, dass du unsere Welt gerettet hast. Aber ich bin wütend – die Zahnarztkosten sind nicht günstiger geworden. Schlechtes Gewissen inklusive.", "dialog_dressler": "Dresslauf die Eins!", - "dialog_intro_1": "Du sitzt an deim Tisch…", - "dialog_intro_2": "Du muasst es schaffn, sonst kommt der Hongkong", - "death_shoe": "Schuah gschenkt kriagt", - "death_mower": "Mähdrescher-Biss", - "death_rooster": "Vom Urhahn totpickt", - "death_quirxel": "Vom Quirxel gquadratet", - "death_default": "Z'Tode gstorbn", + "dialog_intro_1": "Du sitzt an deinem Tisch und spielst dein Lieblingsspiel…", + "dialog_intro_2": "Du musst es schaffen. Du musst es schaffen – sonst kommt der Hongkong.", + "death_shoe": "Da hat dir wohl wer 'nen Schuh geschenkt.", + "death_mower": "Du wurdest von einem Mähdrescher gebissen. Dass sowas mal passiert, hätte ich jetzt nicht gedacht.", + "death_rooster": "Vom Urhahn totgepickt. 10 Sekunden Schock.", + "death_quirxel": "Von einem Quirxel gequadratet.", + "death_default": "Zu Tode gestorben.", "item_tooth": "Zahn", "item_snake": "Schlang", "item_milk": "Selbergmelkte Milch", "item_banana": "Banane", "item_wartchen": "Wartchen", "item_sunglasses": "Coole Sonnenbrille", - "item_death_ticket": "Ticket: Z'Tode gessen", + "item_death_ticket": "Schein der Uralten", "item_moon_sword": "Mondschwert", "item_chicken": "Hendl", "item_cheese": "Verschimmelta Kas", @@ -84,5 +84,60 @@ "ach_tooth_desc": "Zähne beim Zahnarzt klauen", "ach_all_title": "Burger, Burger, Burger. Heit ess i alle Quests", "ach_all_desc": "Alle Achievements freischaltn", - "burger_cham": "Burger! Beste Qualität von der Explosion der Spiele" + "burger_cham": "Burger! Beste Qualität von der Explosion der Spiele", + "title_pay2win": "Pay2Win (damit de Reichen kemma, hihihihi)", + "title_music_hint": "Hey, das hier ist so eine laute Musik extra. Damit du direkt auf Starten drückst…", + "lang_pick_title": "Sproch aussucha", + "lang_de": "Deutsch", + "lang_en": "English", + "lang_by": "Bayerisch", + "dialog_villager_run": "Hongkong! Hongkong ist er! Er hat alles zerstört!", + "dialog_table_hint": "Unter dem Tisch ist ein Knopf…", + "dialog_tiramisu": "Eine Tonne Tiramisu! Du bist jetzt 10 Minuten zu fett. Stimulation: Subway Surf & Satisfying Shorts.", + "dialog_bear_help": "Hilfe! Der Schlangenmähdrescher würgt mich!", + "dialog_bear_saved": "Hongkong hat den Rasenmäher zu Schlangen verwandelt. Nimm meinen Zahn – und bring die Schlangen zur Massage!", + "dialog_snake_pain": "Ich bin sehr verspannt, oh, mir tut alles so weh.", + "dialog_villager_snake": "Der Schlange geht es nicht so gut, ich glaube, sie braucht eine Massage.", + "dialog_cow_drink": "Gluck gluck… ich melk mich selbst. Hier, selbst gemelkte Milch.", + "dialog_cow_massage": "Die Schlange schläft ein… Quest erledigt.", + "dialog_catscene": "Du hast das Dorf verlassen. Nun könntest du in eine Open World… aber das wäre zu viel Arbeit. Keine Open World. Du bist traurig. Ist mir egal.", + "dialog_catscene_skip": "Du willst diese spannende Katzin überspringen? Spinnst du? Strafe: alles von vorne!", + "dialog_mage_push": "Mit der Macht… zurück nach links!", + "dialog_mage_angry": "Ich bin der dunkelgraue Magier – NICHT der schwarze! *explodiert den Planeten*", + "dialog_farmer_direct": "Grrrrirstsel, du bist ja brav und bist gleich hierher gegangen!", + "dialog_farmer_detour": "Grrrrirstsel, du bist also nicht gleich hierher gegangen – ich muss erst den Magier holen. Umtalentiert!", + "dialog_farmer_quest": "Weißt du was? Wennst die Tierregie machst, kriegst von mir 1,99. Schweine füttern, Kühe melken, Hühner sneaken – Vorsicht Urhahn!", + "dialog_urhahn": "KIKERIKIIII – der Urhahn pickt dich tot. 10 Sekunden Schock.", + "dialog_paywall": "Du willst weiterspielen? Zahl jetzt 1,99… oder Dresslerpunkte im Shop.", + "dialog_dressler_drop": "Auf dem Apfel steht: ESSEN NICHT – außer du willst das Spiel durchspielen.", + "dialog_boss_unclever": "Ich weiß, wie du zu mir gelangt bist. Das ist sehr unkreativ, aber wir hatten keine Ideen mehr.", + "dialog_sunglasses_free": "Sonnenbrille? Nein Spaß – gratis!", + "dialog_reveal": "Übrigens: Hongkong war nur ein Prank – er heißt echt Glitschkönig! Hahaha! Du bist voll drauf reingefallen. Belohnung: Banane + Wartchen.", + "dialog_end_angry": "Die Dorfbewohner sind wütend wegen der Zahnarztkosten. Bau trotzdem dein Haus.", + "dialog_toothionaire": "Brich beim Zahnarzt ein und klaue die Zähne – werde Zahnionär!", + "death_glitzo": "⬜", + "death_bowser": "Fake Bowser hat dich aufgegessen.", + "death_cheese": "Verschimmelter Käse im Mund. Pfui.", + "death_ticket_eaten": "Schein der Uralten gegessen. Zu verrückt? Verstanden.", + "death_revive": "Wiederbeleben", + "death_yes": "Ja", + "death_no": "Nein", + "death_yes_hint": "Ja = Spiel beenden (zur Title).", + "death_no_hint": "Nein = trotzdem wiederbeleben. Satire.", + "shop_joke_continue": "Kostet 10 Dresslerpunkte… Spaß, du darfst weiter! Pay2Win war nur für reiche Leute.", + "shop_joke_free": "Nein Spaß, das bekommst du gratis.", + "shop_continue_joke": "Weiter spielen (10 Punkte? Spaß)", + "shop_next_level": "Nächstes Level freikaufen (1 Punkt)", + "shop_burger": "Burger (1 Punkt)", + "item_dressler_apple": "Zertresselter Apfel", + "item_wedding_egg": "Hochzeitsei / Whiff", + "item_lava_chicken": "Lava-Chicken", + "status_fat": "ZU FETT (%s)", + "status_shock": "URHAHN-SCHOCK", + "status_glitch": "VERGLITSCHT", + "choice_massage_now": "Schlange jetzt massieren", + "choice_drink_first": "Erst viel trinken (Secret)", + "choice_hammer_1": "Hammer 1", + "choice_hammer_2": "Hammer 2", + "choice_hammer_3": "Hammer 3" } diff --git a/data/locales/de.json b/data/locales/de.json index f1fe96d..f1dbdb7 100644 --- a/data/locales/de.json +++ b/data/locales/de.json @@ -24,28 +24,28 @@ "dialog_snake": "Ich bin sehr verspannt, oh, mir tut alles so weh", "dialog_bear": "Hongkong hat den Rasenmäher zu Schlangen verwandelt", "dialog_hongkong_npc": "Hongkong, Hongkong ist er!", - "dialog_cow": "Massageoptionen: Schlange / Spieler / Quadrat-Emoji", + "dialog_cow": "Masseur-Kuh: Massiere die Schlange jetzt – oder soll ich erst viel trinken?", "dialog_farmer": "Servus! Die Schweine brauchen Quests und der Urhahn pickt.", "dialog_mage": "Der Weg nach rechts führt zum Bauernhof. Links ist Fake-Open-World.", - "dialog_cheese": "Der Mond ist aus Käse. Willst du eine Sonnenbrille?", - "dialog_bowser": "Ich halte ein Huhn gefangen! (Fake Bowser)", + "dialog_cheese": "Verschimmelter Käse! Nicht den Mund treffen lassen – stech den KÄSE, nicht den Verkäufer!", + "dialog_bowser": "Fake Bowser hält das Huhn! Rate den richtigen Hammer – sonst frisst er dich.", "dialog_hongkong_boss": "Ich werde wiederkommen!", - "dialog_bear_end": "Gut, dass du unsere Welt gerettet hast. Aber die Zahnarztkosten sind immer noch teuer.", + "dialog_bear_end": "Gut, dass du unsere Welt gerettet hast. Aber ich bin wütend – die Zahnarztkosten sind nicht günstiger geworden. Schlechtes Gewissen inklusive.", "dialog_dressler": "Dresslauf die Eins!", - "dialog_intro_1": "Du sitzt an deinem Tisch…", - "dialog_intro_2": "Du musst es schaffen, sonst kommt der Hongkong", - "death_shoe": "Schuh geschenkt", - "death_mower": "Mähdrescher-Biss", - "death_rooster": "Vom Urhahn totgepickt", - "death_quirxel": "Von einem Quirxel gequadratet", - "death_default": "Zu Tode gestorben", + "dialog_intro_1": "Du sitzt an deinem Tisch und spielst dein Lieblingsspiel…", + "dialog_intro_2": "Du musst es schaffen. Du musst es schaffen – sonst kommt der Hongkong.", + "death_shoe": "Da hat dir wohl wer 'nen Schuh geschenkt.", + "death_mower": "Du wurdest von einem Mähdrescher gebissen. Dass sowas mal passiert, hätte ich jetzt nicht gedacht.", + "death_rooster": "Vom Urhahn totgepickt. 10 Sekunden Schock.", + "death_quirxel": "Von einem Quirxel gequadratet.", + "death_default": "Zu Tode gestorben.", "item_tooth": "Zahn", "item_snake": "Schlange", "item_milk": "Selbst gemelkte Milch", "item_banana": "Banane", "item_wartchen": "Wartchen", "item_sunglasses": "Coole Sonnenbrille", - "item_death_ticket": "Ticket: Zu Tode gegessen", + "item_death_ticket": "Schein der Uralten", "item_moon_sword": "Mondschwert", "item_chicken": "Huhn", "item_cheese": "Verschimmelter Käse", @@ -84,5 +84,60 @@ "ach_tooth_desc": "Zähne beim Zahnarzt klauen", "ach_all_title": "Burger, Burger, Burger. Heute ess ich alle Quests", "ach_all_desc": "Alle Achievements freischalten", - "burger_cham": "Burger! Beste Qualität von der Explosion der Spiele" + "burger_cham": "Burger! Beste Qualität von der Explosion der Spiele", + "title_pay2win": "Pay2Win (um reiche Leute anzulocken, hihihihi)", + "title_music_hint": "Hey, das hier ist so eine laute Musik extra. Damit du direkt auf Starten drückst…", + "lang_pick_title": "Sprache wählen", + "lang_de": "Deutsch", + "lang_en": "English", + "lang_by": "Bayerisch", + "dialog_villager_run": "Hongkong! Hongkong ist er! Er hat alles zerstört!", + "dialog_table_hint": "Unter dem Tisch ist ein Knopf…", + "dialog_tiramisu": "Eine Tonne Tiramisu! Du bist jetzt 10 Minuten zu fett. Stimulation: Subway Surf & Satisfying Shorts.", + "dialog_bear_help": "Hilfe! Der Schlangenmähdrescher würgt mich!", + "dialog_bear_saved": "Hongkong hat den Rasenmäher zu Schlangen verwandelt. Nimm meinen Zahn – und bring die Schlangen zur Massage!", + "dialog_snake_pain": "Ich bin sehr verspannt, oh, mir tut alles so weh.", + "dialog_villager_snake": "Der Schlange geht es nicht so gut, ich glaube, sie braucht eine Massage.", + "dialog_cow_drink": "Gluck gluck… ich melk mich selbst. Hier, selbst gemelkte Milch.", + "dialog_cow_massage": "Die Schlange schläft ein… Quest erledigt.", + "dialog_catscene": "Du hast das Dorf verlassen. Nun könntest du in eine Open World… aber das wäre zu viel Arbeit. Keine Open World. Du bist traurig. Ist mir egal.", + "dialog_catscene_skip": "Du willst diese spannende Katzin überspringen? Spinnst du? Strafe: alles von vorne!", + "dialog_mage_push": "Mit der Macht… zurück nach links!", + "dialog_mage_angry": "Ich bin der dunkelgraue Magier – NICHT der schwarze! *explodiert den Planeten*", + "dialog_farmer_direct": "Grrrrirstsel, du bist ja brav und bist gleich hierher gegangen!", + "dialog_farmer_detour": "Grrrrirstsel, du bist also nicht gleich hierher gegangen – ich muss erst den Magier holen. Umtalentiert!", + "dialog_farmer_quest": "Weißt du was? Wennst die Tierregie machst, kriegst von mir 1,99. Schweine füttern, Kühe melken, Hühner sneaken – Vorsicht Urhahn!", + "dialog_urhahn": "KIKERIKIIII – der Urhahn pickt dich tot. 10 Sekunden Schock.", + "dialog_paywall": "Du willst weiterspielen? Zahl jetzt 1,99… oder Dresslerpunkte im Shop.", + "dialog_dressler_drop": "Auf dem Apfel steht: ESSEN NICHT – außer du willst das Spiel durchspielen.", + "dialog_boss_unclever": "Ich weiß, wie du zu mir gelangt bist. Das ist sehr unkreativ, aber wir hatten keine Ideen mehr.", + "dialog_sunglasses_free": "Sonnenbrille? Nein Spaß – gratis!", + "dialog_reveal": "Übrigens: Hongkong war nur ein Prank – er heißt echt Glitschkönig! Hahaha! Du bist voll drauf reingefallen. Belohnung: Banane + Wartchen.", + "dialog_end_angry": "Die Dorfbewohner sind wütend wegen der Zahnarztkosten. Bau trotzdem dein Haus.", + "dialog_toothionaire": "Brich beim Zahnarzt ein und klaue die Zähne – werde Zahnionär!", + "death_glitzo": "⬜", + "death_bowser": "Fake Bowser hat dich aufgegessen.", + "death_cheese": "Verschimmelter Käse im Mund. Pfui.", + "death_ticket_eaten": "Schein der Uralten gegessen. Zu verrückt? Verstanden.", + "death_revive": "Wiederbeleben", + "death_yes": "Ja", + "death_no": "Nein", + "death_yes_hint": "Ja = Spiel beenden (zur Title).", + "death_no_hint": "Nein = trotzdem wiederbeleben. Satire.", + "shop_joke_continue": "Kostet 10 Dresslerpunkte… Spaß, du darfst weiter! Pay2Win war nur für reiche Leute.", + "shop_joke_free": "Nein Spaß, das bekommst du gratis.", + "shop_continue_joke": "Weiter spielen (10 Punkte? Spaß)", + "shop_next_level": "Nächstes Level freikaufen (1 Punkt)", + "shop_burger": "Burger (1 Punkt)", + "item_dressler_apple": "Zertresselter Apfel", + "item_wedding_egg": "Hochzeitsei / Whiff", + "item_lava_chicken": "Lava-Chicken", + "status_fat": "ZU FETT (%s)", + "status_shock": "URHAHN-SCHOCK", + "status_glitch": "VERGLITSCHT", + "choice_massage_now": "Schlange jetzt massieren", + "choice_drink_first": "Erst viel trinken (Secret)", + "choice_hammer_1": "Hammer 1", + "choice_hammer_2": "Hammer 2", + "choice_hammer_3": "Hammer 3" } diff --git a/data/locales/en.json b/data/locales/en.json index f87d585..70aa766 100644 --- a/data/locales/en.json +++ b/data/locales/en.json @@ -20,32 +20,32 @@ "lang_voice_de": "Am deutschen Wesen soll die Welt genesen", "lang_voice_en": "I drink my garden in the cinema", "lang_voice_by": "Schulz", - "dialog_reif": "If you are not ripe yet, I must gift you a shoe!", + "dialog_reif": "Wenn du noch nicht reif bist, muss ich dir einen Schuh schenken!", "dialog_snake": "I am very tense, oh everything hurts", "dialog_bear": "Hongkong turned the lawnmower into snakes", "dialog_hongkong_npc": "Hongkong, Hongkong is he!", - "dialog_cow": "Massage options: Snake / Player / Square Emoji", + "dialog_cow": "Masseur-Kuh: Massiere die Schlange jetzt – oder soll ich erst viel trinken?", "dialog_farmer": "Howdy! Pigs need quests and the Ur-rooster pecks.", "dialog_mage": "Right path leads to the farm. Left is fake open world.", - "dialog_cheese": "The moon is made of cheese. Want sunglasses?", - "dialog_bowser": "I am holding a chicken hostage! (Fake Bowser)", - "dialog_hongkong_boss": "I will return!", - "dialog_bear_end": "Good that you saved our world. But dental costs are still expensive.", - "dialog_dressler": "Dress run the one!", - "dialog_intro_1": "You sit at your desk…", - "dialog_intro_2": "You must succeed, or Hongkong comes", - "death_shoe": "Gifted a shoe", - "death_mower": "Combine-harvester bite", - "death_rooster": "Pecked to death by Ur-rooster", - "death_quirxel": "Squared by a Quirxel", - "death_default": "Died to death", + "dialog_cheese": "Verschimmelter Käse! Nicht den Mund treffen lassen – stech den KÄSE, nicht den Verkäufer!", + "dialog_bowser": "Fake Bowser hält das Huhn! Rate den richtigen Hammer – sonst frisst er dich.", + "dialog_hongkong_boss": "Ich werde wiederkommen!", + "dialog_bear_end": "Gut, dass du unsere Welt gerettet hast. Aber ich bin wütend – die Zahnarztkosten sind nicht günstiger geworden. Schlechtes Gewissen inklusive.", + "dialog_dressler": "Dresslauf die Eins!", + "dialog_intro_1": "Du sitzt an deinem Tisch und spielst dein Lieblingsspiel…", + "dialog_intro_2": "Du musst es schaffen. Du musst es schaffen – sonst kommt der Hongkong.", + "death_shoe": "Da hat dir wohl wer 'nen Schuh geschenkt.", + "death_mower": "Du wurdest von einem Mähdrescher gebissen. Dass sowas mal passiert, hätte ich jetzt nicht gedacht.", + "death_rooster": "Vom Urhahn totgepickt. 10 Sekunden Schock.", + "death_quirxel": "Von einem Quirxel gequadratet.", + "death_default": "Zu Tode gestorben.", "item_tooth": "Tooth", "item_snake": "Snake", "item_milk": "Self-reported milk", "item_banana": "Banana", "item_wartchen": "Wartchen", "item_sunglasses": "Cool sunglasses", - "item_death_ticket": "Ticket: Eaten to death", + "item_death_ticket": "Schein der Uralten", "item_moon_sword": "Moon sword", "item_chicken": "Chicken", "item_cheese": "Moldy cheese", @@ -84,5 +84,60 @@ "ach_tooth_desc": "Steal teeth at the dentist", "ach_all_title": "Burger, Burger, Burger. Today I eat all quests", "ach_all_desc": "Unlock all achievements", - "burger_cham": "Burger! Best quality from Explosion der Spiele" + "burger_cham": "Burger! Beste Qualität von der Explosion der Spiele", + "title_pay2win": "Pay2Win (to attract rich people, hihihihi)", + "title_music_hint": "Hey, das hier ist so eine laute Musik extra. Damit du direkt auf Starten drückst…", + "lang_pick_title": "Choose language", + "lang_de": "Deutsch", + "lang_en": "English", + "lang_by": "Bayerisch", + "dialog_villager_run": "Hongkong! Hongkong ist er! Er hat alles zerstört!", + "dialog_table_hint": "Unter dem Tisch ist ein Knopf…", + "dialog_tiramisu": "Eine Tonne Tiramisu! Du bist jetzt 10 Minuten zu fett. Stimulation: Subway Surf & Satisfying Shorts.", + "dialog_bear_help": "Hilfe! Der Schlangenmähdrescher würgt mich!", + "dialog_bear_saved": "Hongkong hat den Rasenmäher zu Schlangen verwandelt. Nimm meinen Zahn – und bring die Schlangen zur Massage!", + "dialog_snake_pain": "Ich bin sehr verspannt, oh, mir tut alles so weh.", + "dialog_villager_snake": "Der Schlange geht es nicht so gut, ich glaube, sie braucht eine Massage.", + "dialog_cow_drink": "Gluck gluck… ich melk mich selbst. Hier, selbst gemelkte Milch.", + "dialog_cow_massage": "Die Schlange schläft ein… Quest erledigt.", + "dialog_catscene": "Du hast das Dorf verlassen. Nun könntest du in eine Open World… aber das wäre zu viel Arbeit. Keine Open World. Du bist traurig. Ist mir egal.", + "dialog_catscene_skip": "Skip this exciting catscene? Are you nuts? Penalty: start over!", + "dialog_mage_push": "Mit der Macht… zurück nach links!", + "dialog_mage_angry": "Ich bin der dunkelgraue Magier – NICHT der schwarze! *explodiert den Planeten*", + "dialog_farmer_direct": "Grrrrirstsel, du bist ja brav und bist gleich hierher gegangen!", + "dialog_farmer_detour": "Grrrrirstsel, du bist also nicht gleich hierher gegangen – ich muss erst den Magier holen. Umtalentiert!", + "dialog_farmer_quest": "Weißt du was? Wennst die Tierregie machst, kriegst von mir 1,99. Schweine füttern, Kühe melken, Hühner sneaken – Vorsicht Urhahn!", + "dialog_urhahn": "KIKERIKIIII – der Urhahn pickt dich tot. 10 Sekunden Schock.", + "dialog_paywall": "Du willst weiterspielen? Zahl jetzt 1,99… oder Dresslerpunkte im Shop.", + "dialog_dressler_drop": "Auf dem Apfel steht: ESSEN NICHT – außer du willst das Spiel durchspielen.", + "dialog_boss_unclever": "Ich weiß, wie du zu mir gelangt bist. Das ist sehr unkreativ, aber wir hatten keine Ideen mehr.", + "dialog_sunglasses_free": "Sonnenbrille? Nein Spaß – gratis!", + "dialog_reveal": "Übrigens: Hongkong war nur ein Prank – er heißt echt Glitschkönig! Hahaha! Du bist voll drauf reingefallen. Belohnung: Banane + Wartchen.", + "dialog_end_angry": "Die Dorfbewohner sind wütend wegen der Zahnarztkosten. Bau trotzdem dein Haus.", + "dialog_toothionaire": "Brich beim Zahnarzt ein und klaue die Zähne – werde Zahnionär!", + "death_glitzo": "⬜", + "death_bowser": "Fake Bowser hat dich aufgegessen.", + "death_cheese": "Verschimmelter Käse im Mund. Pfui.", + "death_ticket_eaten": "Schein der Uralten gegessen. Zu verrückt? Verstanden.", + "death_revive": "Revive", + "death_yes": "Yes", + "death_no": "No", + "death_yes_hint": "Ja = Spiel beenden (zur Title).", + "death_no_hint": "Nein = trotzdem wiederbeleben. Satire.", + "shop_joke_continue": "Kostet 10 Dresslerpunkte… Spaß, du darfst weiter! Pay2Win war nur für reiche Leute.", + "shop_joke_free": "Nein Spaß, das bekommst du gratis.", + "shop_continue_joke": "Weiter spielen (10 Punkte? Spaß)", + "shop_next_level": "Nächstes Level freikaufen (1 Punkt)", + "shop_burger": "Burger (1 Punkt)", + "item_dressler_apple": "Zertresselter Apfel", + "item_wedding_egg": "Hochzeitsei / Whiff", + "item_lava_chicken": "Lava-Chicken", + "status_fat": "ZU FETT (%s)", + "status_shock": "URHAHN-SCHOCK", + "status_glitch": "VERGLITSCHT", + "choice_massage_now": "Schlange jetzt massieren", + "choice_drink_first": "Erst viel trinken (Secret)", + "choice_hammer_1": "Hammer 1", + "choice_hammer_2": "Hammer 2", + "choice_hammer_3": "Hammer 3" } diff --git a/project.godot b/project.godot index 3ee0723..7e8f2a8 100644 --- a/project.godot +++ b/project.godot @@ -1,5 +1,10 @@ ; Engine configuration file. -; Explosion der Spiele — Pay2Win Chaos Scaffold (Godot 4.3+) +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters config_version=5 @@ -91,4 +96,3 @@ skip_cutscene={ [rendering] textures/canvas_textures/default_texture_filter=0 -anti_aliasing/quality/msaa_3d=0 diff --git a/scenes/catscene.tscn b/scenes/catscene.tscn new file mode 100644 index 0000000..2cd351b --- /dev/null +++ b/scenes/catscene.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/levels/catscene.gd" id="1"] + +[node name="Catscene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="Bg" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.15, 0.12, 0.2, 1) + +[node name="Cat" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -80.0 +offset_top = -120.0 +offset_right = 80.0 +offset_bottom = -20.0 +color = Color(1, 0.6, 0.2, 1) + +[node name="Label" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -420.0 +offset_top = 40.0 +offset_right = 420.0 +offset_bottom = 200.0 +horizontal_alignment = 1 +autowrap_mode = 3 +text = "..." + +[node name="ContinueButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -140.0 +offset_top = -140.0 +offset_right = 140.0 +offset_bottom = -90.0 +text = "Weiter" + +[node name="SkipButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -140.0 +offset_top = -80.0 +offset_right = 140.0 +offset_bottom = -30.0 +text = "Skip" + +[connection signal="pressed" from="ContinueButton" to="." method="_on_continue_pressed"] +[connection signal="pressed" from="SkipButton" to="." method="_on_skip_pressed"] diff --git a/scenes/death_screen.tscn b/scenes/death_screen.tscn index df2f860..c78c7be 100644 --- a/scenes/death_screen.tscn +++ b/scenes/death_screen.tscn @@ -16,7 +16,7 @@ layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -color = Color(0.7, 0.1, 0.5, 1) +color = Color(0, 0, 0, 1) [node name="Title" type="Label" parent="."] layout_mode = 1 @@ -24,9 +24,9 @@ anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 offset_left = -300.0 -offset_top = 120.0 +offset_top = 80.0 offset_right = 300.0 -offset_bottom = 180.0 +offset_bottom = 140.0 theme_override_font_sizes/font_size = 36 horizontal_alignment = 1 text = "DU BIST GESTORBEN" @@ -36,13 +36,26 @@ layout_mode = 1 anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -300.0 -offset_top = 200.0 -offset_right = 300.0 -offset_bottom = 250.0 +offset_left = -360.0 +offset_top = 160.0 +offset_right = 360.0 +offset_bottom = 260.0 horizontal_alignment = 1 +autowrap_mode = 3 text = "..." +[node name="Hint" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -360.0 +offset_top = 280.0 +offset_right = 360.0 +offset_bottom = 360.0 +horizontal_alignment = 1 +autowrap_mode = 3 + [node name="Skull" type="ColorRect" parent="."] visible = false layout_mode = 1 @@ -51,13 +64,13 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -40.0 -offset_top = -40.0 -offset_right = 40.0 -offset_bottom = 40.0 +offset_left = -60.0 +offset_top = -60.0 +offset_right = 60.0 +offset_bottom = 60.0 color = Color(1, 1, 1, 1) -[node name="ContinueButton" type="Button" parent="."] +[node name="ReviveButton" type="Button" parent="."] layout_mode = 1 anchors_preset = 7 anchor_left = 0.5 @@ -65,9 +78,37 @@ anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -160.0 -offset_top = -120.0 +offset_top = -200.0 offset_right = 160.0 -offset_bottom = -70.0 -text = "Continue" +offset_bottom = -150.0 +text = "Wiederbeleben" -[connection signal="pressed" from="ContinueButton" to="." method="_on_continue_pressed"] +[node name="NoButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -200.0 +offset_top = -130.0 +offset_right = -20.0 +offset_bottom = -80.0 +text = "Nein" + +[node name="YesButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = -130.0 +offset_right = 200.0 +offset_bottom = -80.0 +text = "Ja" + +[connection signal="pressed" from="ReviveButton" to="." method="_on_revive_pressed"] +[connection signal="pressed" from="NoButton" to="." method="_on_no_pressed"] +[connection signal="pressed" from="YesButton" to="." method="_on_yes_pressed"] diff --git a/scenes/dressler_boss.tscn b/scenes/dressler_boss.tscn new file mode 100644 index 0000000..fdbe147 --- /dev/null +++ b/scenes/dressler_boss.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=6 format=3] + +[ext_resource type="Script" path="res://scripts/levels/dressler_boss.gd" id="1"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/player.tscn" id="2"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/enemy.tscn" id="3"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/exit_trigger.tscn" id="4"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/hazard.tscn" id="5"] + +[sub_resource type="BoxMesh" id="BoxMesh_ground"] +size = Vector3(40, 0.2, 40) + +[sub_resource type="StandardMaterial3D" id="Mat_ground"] +albedo_color = Color(0.35, 0.55, 0.2, 1) +shading_mode = 0 + +[sub_resource type="BoxShape3D" id="BoxShape_ground"] +size = Vector3(40, 0.2, 40) + +[node name="dressler_boss" type="Node3D"] +script = ExtResource("1") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) + +[node name="Ground" type="StaticBody3D" parent="."] +collision_layer = 1 + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"] +mesh = SubResource("BoxMesh_ground") +surface_material_override/0 = SubResource("Mat_ground") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"] +shape = SubResource("BoxShape_ground") + +[node name="Player" parent="." instance=ExtResource("2")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 6) + +[node name="Dressler" parent="." instance=ExtResource("3")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -3) +max_hp = 10 +texture_path = "res://assets/sprites/placeholders/apple.png" +defeat_flag = "dressler_defeated" +achievement_on_defeat = "dressler_apples" + +[node name="Mower1" parent="." instance=ExtResource("5")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.1, -1) +death_cause_key = "death_mower" + +[node name="Mower2" parent="." instance=ExtResource("5")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0.1, -1) +death_cause_key = "death_mower" + +[node name="Exit" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -12) +require_flag = "dressler_defeated" diff --git a/scenes/end_screen.tscn b/scenes/end_screen.tscn index 1599994..7a64f06 100644 --- a/scenes/end_screen.tscn +++ b/scenes/end_screen.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -45,6 +41,12 @@ shape = SubResource("BoxShape_ground") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 4) + +[node name="DentistHeist" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -5) +dialog_key = "dialog_toothionaire" +texture_path = "res://assets/sprites/placeholders/npc.png" + [node name="BearEnd" parent="." instance=ExtResource("4")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.1, 0) dialog_key = "dialog_bear_end" diff --git a/scenes/farm.tscn b/scenes/farm.tscn index 1515116..5cf9976 100644 --- a/scenes/farm.tscn +++ b/scenes/farm.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -49,7 +45,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 4) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0.1, 0) dialog_key = "dialog_farmer" texture_path = "res://assets/sprites/placeholders/farmer.png" -give_item_id = "chicken" [node name="Urhahn" parent="." instance=ExtResource("5")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.1, -2) @@ -62,14 +57,34 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.1, -4) death_cause_key = "death_rooster" -[node name="Dressler" parent="." instance=ExtResource("5")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0.1, -4) -max_hp = 5 -texture_path = "res://assets/sprites/placeholders/apple.png" -defeat_flag = "dressler_defeated" -achievement_on_defeat = "dressler_apples" + + +[node name="PigQuest" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0.1, 2) +dialog_key = "dialog_farmer_quest" +texture_path = "res://assets/sprites/placeholders/npc.png" +quest_flag = "pigs_fed" + +[node name="CowQuest" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0.1, 2) +dialog_key = "dialog_farmer_quest" +texture_path = "res://assets/sprites/placeholders/cow.png" +quest_flag = "cows_milked" + +[node name="ChickenQuest" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.1, -6) +dialog_key = "dialog_urhahn" +texture_path = "res://assets/sprites/placeholders/rooster.png" +give_item_id = "chicken" +quest_flag = "chickens_taken" + + +[node name="TicketVendor" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0.1, -8) +dialog_key = "item_death_ticket" +texture_path = "res://assets/sprites/placeholders/npc.png" [node name="Exit" parent="." instance=ExtResource("3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -12) -require_flag = "urhahn_defeated" +require_flag = "paywall_cleared" diff --git a/scenes/farm_path.tscn b/scenes/farm_path.tscn index 89c6ad2..13b65e2 100644 --- a/scenes/farm_path.tscn +++ b/scenes/farm_path.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -51,7 +47,13 @@ dialog_key = "dialog_mage" texture_path = "res://assets/sprites/placeholders/mage.png" quest_flag = "mage_talked" -[node name="Exit" parent="." instance=ExtResource("3")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0.1, -10) -require_flag = "mage_talked" +[node name="LeftExit" parent="." instance=ExtResource("3")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0.1, -10) +use_goto_next = false +next_scene_id = "farm" + +[node name="RightZone" parent="." instance=ExtResource("3")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0.1, -6) +use_goto_next = false +next_scene_id = "" diff --git a/scenes/final_boss.tscn b/scenes/final_boss.tscn index e2aebc4..7e406c5 100644 --- a/scenes/final_boss.tscn +++ b/scenes/final_boss.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 diff --git a/scenes/massage_studio.tscn b/scenes/massage_studio.tscn index d16f333..306c82b 100644 --- a/scenes/massage_studio.tscn +++ b/scenes/massage_studio.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -49,11 +45,47 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 4) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -2) dialog_key = "dialog_cow" texture_path = "res://assets/sprites/placeholders/cow.png" -require_item_id = "snake" -consume_required = true -quest_flag = "snake_massaged" -achievement_id = "snake_massage" [node name="Exit" parent="." instance=ExtResource("3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -10) + +[node name="ChoiceUI" type="CanvasLayer" parent="."] +layer = 50 + +[node name="Panel" type="PanelContainer" parent="ChoiceUI"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -220.0 +offset_top = -100.0 +offset_right = 220.0 +offset_bottom = 100.0 + +[node name="Margin" type="MarginContainer" parent="ChoiceUI/Panel"] +layout_mode = 2 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 10 + +[node name="VBox" type="VBoxContainer" parent="ChoiceUI/Panel/Margin"] +layout_mode = 2 + +[node name="Label" type="Label" parent="ChoiceUI/Panel/Margin/VBox"] +layout_mode = 2 +autowrap_mode = 3 +text = "Kuh" + +[node name="MassageBtn" type="Button" parent="ChoiceUI/Panel/Margin/VBox"] +layout_mode = 2 +text = "Massage" + +[node name="DrinkBtn" type="Button" parent="ChoiceUI/Panel/Margin/VBox"] +layout_mode = 2 +text = "Trinken" + +[connection signal="pressed" from="ChoiceUI/Panel/Margin/VBox/MassageBtn" to="." method="_on_massage_pressed"] +[connection signal="pressed" from="ChoiceUI/Panel/Margin/VBox/DrinkBtn" to="." method="_on_drink_pressed"] diff --git a/scenes/meadow.tscn b/scenes/meadow.tscn index 00f9e49..1e78267 100644 --- a/scenes/meadow.tscn +++ b/scenes/meadow.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -60,6 +56,12 @@ texture_path = "res://assets/sprites/placeholders/bear.png" transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -5) death_cause_key = "death_mower" + +[node name="SnakePickup" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0.1, -4) +dialog_key = "dialog_snake_pain" +texture_path = "res://assets/sprites/placeholders/snake.png" + [node name="Exit" parent="." instance=ExtResource("3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -12) diff --git a/scenes/moon.tscn b/scenes/moon.tscn index 28501f9..f40d1f6 100644 --- a/scenes/moon.tscn +++ b/scenes/moon.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -57,14 +53,63 @@ dialog_key = "dialog_cheese" texture_path = "res://assets/sprites/placeholders/apple.png" give_item_id = "moon_sword" -[node name="FakeBowser" parent="." instance=ExtResource("5")] +[node name="FakeBowser" parent="." instance=ExtResource("4")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -4) -max_hp = 8 +dialog_key = "dialog_bowser" texture_path = "res://assets/sprites/placeholders/bowser.png" -defeat_flag = "bowser_defeated" -achievement_on_defeat = "beat_bowser" + + +[node name="MoonResearcher" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0.1, -6) +dialog_key = "dialog_sunglasses_free" +texture_path = "res://assets/sprites/placeholders/npc.png" [node name="Exit" parent="." instance=ExtResource("3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -12) require_flag = "bowser_defeated" + +[node name="HammerUI" type="CanvasLayer" parent="."] +layer = 50 + +[node name="Panel" type="PanelContainer" parent="HammerUI"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -110.0 +offset_right = 200.0 +offset_bottom = 110.0 + +[node name="Margin" type="MarginContainer" parent="HammerUI/Panel"] +layout_mode = 2 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 10 + +[node name="VBox" type="VBoxContainer" parent="HammerUI/Panel/Margin"] +layout_mode = 2 + +[node name="Label" type="Label" parent="HammerUI/Panel/Margin/VBox"] +layout_mode = 2 +autowrap_mode = 3 +text = "Hammer" + +[node name="H1" type="Button" parent="HammerUI/Panel/Margin/VBox"] +layout_mode = 2 +text = "Hammer 1" + +[node name="H2" type="Button" parent="HammerUI/Panel/Margin/VBox"] +layout_mode = 2 +text = "Hammer 2" + +[node name="H3" type="Button" parent="HammerUI/Panel/Margin/VBox"] +layout_mode = 2 +text = "Hammer 3" + +[connection signal="pressed" from="HammerUI/Panel/Margin/VBox/H1" to="." method="_on_h1"] +[connection signal="pressed" from="HammerUI/Panel/Margin/VBox/H2" to="." method="_on_h2"] +[connection signal="pressed" from="HammerUI/Panel/Margin/VBox/H3" to="." method="_on_h3"] diff --git a/scenes/prefabs/cheese_projectile.tscn b/scenes/prefabs/cheese_projectile.tscn new file mode 100644 index 0000000..e8cdb09 --- /dev/null +++ b/scenes/prefabs/cheese_projectile.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/combat/cheese_projectile.gd" id="1"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/billboard_sprite.tscn" id="2"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_1"] +radius = 0.35 + +[node name="CheeseProjectile" type="Area3D"] +collision_layer = 4 +collision_mask = 2 +monitoring = true +script = ExtResource("1") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("SphereShape3D_1") + +[node name="BillboardSprite" parent="." instance=ExtResource("2")] diff --git a/scenes/prefabs/cheese_seller.tscn b/scenes/prefabs/cheese_seller.tscn new file mode 100644 index 0000000..3867d1c --- /dev/null +++ b/scenes/prefabs/cheese_seller.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/combat/cheese_seller.gd" id="1"] +[ext_resource type="PackedScene" path="res://scenes/prefabs/billboard_sprite.tscn" id="2"] + +[node name="CheeseSeller" type="Node3D"] +script = ExtResource("1") + +[node name="BillboardSprite" parent="." instance=ExtResource("2")] diff --git a/scenes/prefabs/enemy.tscn b/scenes/prefabs/enemy.tscn index 2648cb0..63d5bcd 100644 --- a/scenes/prefabs/enemy.tscn +++ b/scenes/prefabs/enemy.tscn @@ -8,4 +8,4 @@ script = ExtResource("1") max_hp = 4 [node name="BillboardSprite" parent="." instance=ExtResource("2")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) diff --git a/scenes/prefabs/npc.tscn b/scenes/prefabs/npc.tscn index 0ddea20..1945b64 100644 --- a/scenes/prefabs/npc.tscn +++ b/scenes/prefabs/npc.tscn @@ -7,4 +7,4 @@ script = ExtResource("1") [node name="BillboardSprite" parent="." instance=ExtResource("2")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) diff --git a/scenes/prefabs/player.tscn b/scenes/prefabs/player.tscn index 1ee4200..2aef6d7 100644 --- a/scenes/prefabs/player.tscn +++ b/scenes/prefabs/player.tscn @@ -7,6 +7,9 @@ radius = 0.35 height = 1.0 +[sub_resource type="SphereShape3D" id="SphereShape3D_spring"] +radius = 0.2 + [node name="Player" type="CharacterBody3D"] collision_layer = 2 collision_mask = 1 @@ -17,4 +20,17 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) shape = SubResource("CapsuleShape3D_1") [node name="BillboardSprite" parent="." instance=ExtResource("2")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) + +[node name="CameraYaw" type="Node3D" parent="."] + +[node name="SpringArm3D" type="SpringArm3D" parent="CameraYaw"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0) +spring_length = 5.5 +collision_mask = 1 +margin = 0.2 +shape = SubResource("SphereShape3D_spring") + +[node name="Camera3D" type="Camera3D" parent="CameraYaw/SpringArm3D"] +current = true +fov = 70.0 diff --git a/scenes/shoe_boss.tscn b/scenes/shoe_boss.tscn index 093c3c6..81d944e 100644 --- a/scenes/shoe_boss.tscn +++ b/scenes/shoe_boss.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 diff --git a/scenes/title_screen.tscn b/scenes/title_screen.tscn index bf1019f..6182e7c 100644 --- a/scenes/title_screen.tscn +++ b/scenes/title_screen.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=2 format=3] +[gd_scene load_steps=2 format=3 uid="uid://dbw7nd6f3bxoy"] [ext_resource type="Script" path="res://scripts/levels/title_screen.gd" id="1"] @@ -28,35 +28,50 @@ anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 offset_left = -360.0 -offset_top = 80.0 +offset_top = 64.0 offset_right = 360.0 -offset_bottom = 140.0 +offset_bottom = 124.0 grow_horizontal = 2 theme_override_font_sizes/font_size = 40 -horizontal_alignment = 1 text = "Explosion der Spiele" +horizontal_alignment = 1 [node name="Subtitle" type="Label" parent="."] layout_mode = 1 anchors_preset = 5 anchor_left = 0.5 anchor_right = 0.5 -offset_left = -360.0 -offset_top = 140.0 -offset_right = 360.0 -offset_bottom = 180.0 +offset_left = -420.0 +offset_top = 124.0 +offset_right = 420.0 +offset_bottom = 170.0 grow_horizontal = 2 -horizontal_alignment = 1 text = "Pay2Win" +horizontal_alignment = 1 +autowrap_mode = 3 + +[node name="MusicHint" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -400.0 +offset_top = 180.0 +offset_right = 400.0 +offset_bottom = 240.0 +grow_horizontal = 2 +text = "LAUTE MUSIK" +horizontal_alignment = 1 +autowrap_mode = 3 [node name="Bubble" type="Label" parent="."] layout_mode = 0 offset_left = 40.0 -offset_top = 500.0 +offset_top = 480.0 offset_right = 420.0 -offset_bottom = 620.0 -autowrap_mode = 3 +offset_bottom = 600.0 text = "..." +autowrap_mode = 3 [node name="Disturbed" type="ColorRect" parent="."] layout_mode = 0 @@ -81,4 +96,47 @@ grow_horizontal = 2 grow_vertical = 0 text = "Starten" +[node name="LangPanel" type="PanelContainer" parent="."] +visible = false +layout_mode = 0 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -180.0 +offset_top = -120.0 +offset_right = 180.0 +offset_bottom = 120.0 + +[node name="Margin" type="MarginContainer" parent="LangPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 12 + +[node name="VBox" type="VBoxContainer" parent="LangPanel/Margin"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Title" type="Label" parent="LangPanel/Margin/VBox"] +layout_mode = 2 +text = "Sprache" +horizontal_alignment = 1 + +[node name="BtnDE" type="Button" parent="LangPanel/Margin/VBox"] +layout_mode = 2 +text = "Deutsch" + +[node name="BtnEN" type="Button" parent="LangPanel/Margin/VBox"] +layout_mode = 2 +text = "English" + +[node name="BtnBY" type="Button" parent="LangPanel/Margin/VBox"] +layout_mode = 2 +text = "Bayerisch" + [connection signal="pressed" from="StartButton" to="." method="_on_start_pressed"] +[connection signal="pressed" from="LangPanel/Margin/VBox/BtnDE" to="." method="_on_lang_de"] +[connection signal="pressed" from="LangPanel/Margin/VBox/BtnEN" to="." method="_on_lang_en"] +[connection signal="pressed" from="LangPanel/Margin/VBox/BtnBY" to="." method="_on_lang_by"] diff --git a/scenes/ui/ui_root.tscn b/scenes/ui/ui_root.tscn index 85984f7..02fe756 100644 --- a/scenes/ui/ui_root.tscn +++ b/scenes/ui/ui_root.tscn @@ -195,3 +195,25 @@ layout_mode = 2 [connection signal="value_changed" from="PausePanel/Margin/VBox/MusicSlider" to="." method="_on_music_changed"] [connection signal="item_selected" from="PausePanel/Margin/VBox/LangOption" to="." method="_on_lang_selected"] [connection signal="item_activated" from="PausePanel/Margin/VBox/InventoryList" to="." method="_on_inventory_activated"] + +[node name="FatStim" type="ColorRect" parent="."] +visible = false +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +color = Color(0.05, 0.05, 0.08, 0.55) + +[node name="FatLabel" type="Label" parent="FatStim"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -280.0 +offset_top = -40.0 +offset_right = 280.0 +offset_bottom = 40.0 +horizontal_alignment = 1 +vertical_alignment = 1 +text = "SUBWAY SURF / SATISFYING SHORTS" diff --git a/scenes/village.tscn b/scenes/village.tscn index cc06cb5..ad51d27 100644 --- a/scenes/village.tscn +++ b/scenes/village.tscn @@ -26,10 +26,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 @@ -49,7 +45,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 4) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0.1, 0) dialog_key = "dialog_hongkong_npc" texture_path = "res://assets/sprites/placeholders/npc.png" -give_item_id = "snake" [node name="House" parent="." instance=ExtResource("4")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0.1, -2) @@ -61,9 +56,13 @@ texture_path = "res://assets/sprites/placeholders/house.png" transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0.1, 2) dialog_key = "dialog_hongkong_npc" texture_path = "res://assets/sprites/placeholders/npc.png" -give_item_id = "tooth" -quest_flag = "teeth_stolen" -achievement_id = "tooth_millionaire" + + +[node name="TableButton" parent="." instance=ExtResource("4")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 1) +dialog_key = "dialog_table_hint" +texture_path = "res://assets/sprites/placeholders/house.png" +on_success_goto = "shoe_boss" [node name="Exit" parent="." instance=ExtResource("3")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, -10) diff --git a/scripts/autoload/audio_director.gd b/scripts/autoload/audio_director.gd index 788e4e2..bb06f68 100644 --- a/scripts/autoload/audio_director.gd +++ b/scripts/autoload/audio_director.gd @@ -24,7 +24,7 @@ func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS _music = AudioStreamPlayer.new() _music.bus = "Music" - _music.volume_db = 6.0 # intentionally loud + _music.volume_db = 12.0 # intentionally painfully loud add_child(_music) _sfx = AudioStreamPlayer.new() _sfx.bus = "SFX" @@ -48,7 +48,7 @@ func play_title_music() -> void: if stream == null: return _music.stream = stream - _music.volume_db = 6.0 + _music.volume_db = 12.0 if not _music.playing: _music.play() diff --git a/scripts/autoload/game_state.gd b/scripts/autoload/game_state.gd index b6d9bd7..b204643 100644 --- a/scripts/autoload/game_state.gd +++ b/scripts/autoload/game_state.gd @@ -1,25 +1,12 @@ extends Node -## Global game flow, HP, death, playtime, save/load. -## TODO: Expand save schema when real progression exists. +## Global game flow matching DESIGN.md. signal hp_changed(hp: int, max_hp: int) signal died(cause_key: String) signal scene_changed(scene_id: String) +signal status_changed const SAVE_PATH := "user://save.json" -const SCENE_ORDER: Array[String] = [ - "title_screen", - "intro_cutscene", - "village", - "shoe_boss", - "meadow", - "massage_studio", - "farm_path", - "farm", - "moon", - "final_boss", - "end_screen", -] const SCENE_PATHS := { "title_screen": "res://scenes/title_screen.tscn", @@ -28,23 +15,47 @@ const SCENE_PATHS := { "shoe_boss": "res://scenes/shoe_boss.tscn", "meadow": "res://scenes/meadow.tscn", "massage_studio": "res://scenes/massage_studio.tscn", + "catscene": "res://scenes/catscene.tscn", "farm_path": "res://scenes/farm_path.tscn", "farm": "res://scenes/farm.tscn", + "dressler_boss": "res://scenes/dressler_boss.tscn", "moon": "res://scenes/moon.tscn", "final_boss": "res://scenes/final_boss.tscn", "end_screen": "res://scenes/end_screen.tscn", "death_screen": "res://scenes/death_screen.tscn", } +## Default linear beats (branches override via goto_scene / flags). +const SCENE_ORDER: Array[String] = [ + "title_screen", + "intro_cutscene", + "village", + "shoe_boss", + "meadow", + "massage_studio", + "catscene", + "farm_path", + "farm", + "dressler_boss", + "moon", + "final_boss", + "end_screen", +] + var current_scene_id: String = "title_screen" -var max_hp: int = 5 -var hp: int = 5 +var max_hp: int = 4 +var hp: int = 4 var death_count: int = 0 var last_death_cause_key: String = "death_default" var playtime_seconds: float = 0.0 var house_built: bool = false var flags: Dictionary = {} +## Status: fat (tiramisu), glitched (3 glitzo hits), urhahn shock +var fat_time_left: float = 0.0 +var glitch_hits: int = 0 +var shock_time_left: float = 0.0 + func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS load_game() @@ -57,6 +68,42 @@ func _process(delta: float) -> void: playtime_seconds += delta if playtime_seconds >= 3600.0: Achievements.unlock("played_hour") + var dirty := false + if fat_time_left > 0.0: + fat_time_left = max(0.0, fat_time_left - delta) + dirty = true + if shock_time_left > 0.0: + shock_time_left = max(0.0, shock_time_left - delta) + dirty = true + if dirty: + status_changed.emit() + +func is_fat() -> bool: + return fat_time_left > 0.0 + +func is_shocked() -> bool: + return shock_time_left > 0.0 + +func is_glitched_slow() -> bool: + return glitch_hits >= 3 + +func apply_fat(seconds: float = 600.0) -> void: + fat_time_left = max(fat_time_left, seconds) + Achievements.unlock("tiramisu_tonne") + status_changed.emit() + +func apply_shock(seconds: float = 10.0) -> void: + shock_time_left = max(shock_time_left, seconds) + status_changed.emit() + +func register_glitzo_hit() -> void: + glitch_hits += 1 + status_changed.emit() + take_damage(1, "death_glitzo") + +func clear_glitch() -> void: + glitch_hits = 0 + status_changed.emit() func goto_scene(scene_id: String) -> void: if not SCENE_PATHS.has(scene_id): @@ -70,28 +117,64 @@ func goto_scene(scene_id: String) -> void: get_tree().change_scene_to_file(SCENE_PATHS[scene_id]) func goto_next() -> void: - var idx := SCENE_ORDER.find(current_scene_id) - if idx < 0: - goto_scene("village") - return - if idx >= SCENE_ORDER.size() - 1: - goto_scene("end_screen") - return - goto_scene(SCENE_ORDER[idx + 1]) + match current_scene_id: + "village": + goto_scene("shoe_boss") + "shoe_boss": + goto_scene("meadow") + "meadow": + goto_scene("massage_studio") + "massage_studio": + goto_scene("catscene") + "catscene": + goto_scene("farm_path") + "farm_path": + if get_flag("planet_destroyed", false): + goto_scene("moon") + else: + goto_scene("farm") + "farm": + goto_scene("dressler_boss") + "dressler_boss": + goto_scene("moon") + "moon": + goto_scene("final_boss") + "final_boss": + goto_scene("end_screen") + _: + var idx := SCENE_ORDER.find(current_scene_id) + if idx < 0 or idx >= SCENE_ORDER.size() - 1: + goto_scene("end_screen") + else: + goto_scene(SCENE_ORDER[idx + 1]) func skip_to_final_boss() -> void: Achievements.unlock("milk_skip") flags["milk_skip_used"] = true goto_scene("final_boss") +func reset_to_intro() -> void: + ## Catscene skip punishment. + hp = max_hp + glitch_hits = 0 + fat_time_left = 0.0 + shock_time_left = 0.0 + flags.clear() + Inventory.deserialize({"items": {}}) + goto_scene("intro_cutscene") + func start_new_game() -> void: hp = max_hp + glitch_hits = 0 + fat_time_left = 0.0 + shock_time_left = 0.0 flags.clear() house_built = false goto_scene("intro_cutscene") func heal_full() -> void: hp = max_hp + glitch_hits = 0 hp_changed.emit(hp, max_hp) func take_damage(amount: int = 1, cause_key: String = "death_default") -> void: @@ -109,6 +192,18 @@ func die(cause_key: String = "death_default") -> void: save_game() goto_scene("death_screen") +func revive_continue() -> void: + heal_full() + ## Return to last non-death gameplay scene, default village. + var back := str(flags.get("last_gameplay_scene", "village")) + if back == "death_screen" or back == "title_screen": + back = "village" + goto_scene(back) + +func quit_to_title() -> void: + heal_full() + goto_scene("title_screen") + func set_flag(key: String, value: Variant = true) -> void: flags[key] = value save_game() @@ -116,7 +211,12 @@ func set_flag(key: String, value: Variant = true) -> void: func get_flag(key: String, default_value: Variant = false) -> Variant: return flags.get(key, default_value) +func remember_gameplay_scene() -> void: + if current_scene_id not in ["title_screen", "death_screen", "intro_cutscene", "catscene"]: + flags["last_gameplay_scene"] = current_scene_id + func save_game() -> void: + remember_gameplay_scene() var data := { "hp": hp, "max_hp": max_hp, @@ -126,6 +226,8 @@ func save_game() -> void: "current_scene_id": current_scene_id, "house_built": house_built, "flags": flags, + "fat_time_left": fat_time_left, + "glitch_hits": glitch_hits, "inventory": Inventory.serialize(), "achievements": Achievements.serialize(), "shop": Shop.serialize(), @@ -133,7 +235,6 @@ func save_game() -> void: } var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE) if file == null: - push_warning("Could not write save file") return file.store_string(JSON.stringify(data, "\t")) @@ -155,6 +256,8 @@ func load_game() -> void: current_scene_id = str(data.get("current_scene_id", "title_screen")) house_built = bool(data.get("house_built", false)) flags = data.get("flags", {}) + fat_time_left = float(data.get("fat_time_left", 0.0)) + glitch_hits = int(data.get("glitch_hits", 0)) Inventory.deserialize(data.get("inventory", {})) Achievements.deserialize(data.get("achievements", {})) Shop.deserialize(data.get("shop", {})) diff --git a/scripts/autoload/inventory.gd b/scripts/autoload/inventory.gd index 8cf62b9..498ea8d 100644 --- a/scripts/autoload/inventory.gd +++ b/scripts/autoload/inventory.gd @@ -4,7 +4,7 @@ extends Node signal inventory_changed signal item_used(item_id: String) -var _items: Dictionary = {} # id -> count +var _items: Dictionary = {} var _defs: Dictionary = {} func _ready() -> void: @@ -13,7 +13,6 @@ func _ready() -> void: func _load_defs() -> void: var file := FileAccess.open("res://data/items.json", FileAccess.READ) if file == null: - push_error("Missing items.json") return var parsed: Variant = JSON.parse_string(file.get_as_text()) if typeof(parsed) != TYPE_ARRAY: @@ -64,13 +63,30 @@ func use_item(item_id: String) -> bool: remove_item("death_ticket", 1) Achievements.unlock("eaten_to_death") item_used.emit(item_id) + GameState.die("death_ticket_eaten") return true "tiramisu": remove_item("tiramisu", 1) - var eaten := int(GameState.get_flag("tiramisu_kg", 0)) + 1 - GameState.set_flag("tiramisu_kg", eaten) - if eaten >= 1000: - Achievements.unlock("tiramisu_tonne") + GameState.apply_fat(600.0) + item_used.emit(item_id) + return true + "dressler_apple": + remove_item("dressler_apple", 1) + item_used.emit(item_id) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_boss_unclever"), 4.0) + GameState.goto_scene("final_boss") + return true + "wedding_egg": + remove_item("wedding_egg", 1) + item_used.emit(item_id) + GameState.set_flag("used_whiff", true) + GameState.goto_scene("final_boss") + return true + "banana": + remove_item("banana", 1) + GameState.heal_full() item_used.emit(item_id) return true _: diff --git a/scripts/autoload/settings.gd b/scripts/autoload/settings.gd index 09f9786..3f51c3d 100644 --- a/scripts/autoload/settings.gd +++ b/scripts/autoload/settings.gd @@ -11,11 +11,14 @@ var language: String = "de" var sfx_volume: float = 1.0 ## Intentionally locked loud — slider is UI-disabled. var music_volume: float = 1.0 +var language_chosen: bool = false func _ready() -> void: _ensure_buses() apply_audio() - # Locale reloads after both Settings + Locale exist (Locale._ready / deserialize). + +func needs_language_picker() -> bool: + return not language_chosen func _ensure_buses() -> void: _ensure_bus(MUSIC_BUS) @@ -34,6 +37,7 @@ func set_language(lang: String) -> void: if lang not in ["de", "en", "by"]: return language = lang + language_chosen = true Locale.reload() AudioDirector.play_language_voice(lang) settings_changed.emit() @@ -46,7 +50,6 @@ func set_sfx_volume(value: float) -> void: GameState.save_game() func set_music_volume(_value: float) -> void: - ## Satire lock: music stays maxed. music_volume = 1.0 apply_audio() settings_changed.emit() @@ -68,12 +71,14 @@ func serialize() -> Dictionary: "language": language, "sfx_volume": sfx_volume, "music_volume": 1.0, + "language_chosen": language_chosen, } func deserialize(data: Dictionary) -> void: language = str(data.get("language", "de")) sfx_volume = float(data.get("sfx_volume", 1.0)) music_volume = 1.0 + language_chosen = bool(data.get("language_chosen", false)) Locale.reload() apply_audio() settings_changed.emit() diff --git a/scripts/autoload/shop.gd b/scripts/autoload/shop.gd index d5ca810..5e60b55 100644 --- a/scripts/autoload/shop.gd +++ b/scripts/autoload/shop.gd @@ -8,14 +8,14 @@ var dressler_points: int = 0 const CATALOG := [ {"id": "buy_point", "cost": 0, "mock_euro": true}, - {"id": "sunglasses", "cost": 5, "gives": "sunglasses"}, - {"id": "death_ticket", "cost": 3, "gives": "death_ticket"}, - {"id": "milk", "cost": 10, "gives": "milk"}, - {"id": "tiramisu", "cost": 1, "gives": "tiramisu"}, + {"id": "continue_joke", "cost": 10, "joke_continue": true}, + {"id": "death_ticket", "cost": 1, "gives": "death_ticket"}, + {"id": "sunglasses", "cost": 1, "gives": "sunglasses", "actually_free": true}, + {"id": "burger", "cost": 1, "gives": "tiramisu"}, + {"id": "next_level", "cost": 1, "unlock_next": true}, ] func buy_mock_euro_point() -> void: - ## Satire: pretend the user paid 1€. dressler_points += 1 points_changed.emit(dressler_points) GameState.save_game() @@ -27,6 +27,25 @@ func purchase(entry_id: String) -> bool: for entry in CATALOG: if str(entry["id"]) != entry_id: continue + if bool(entry.get("actually_free", false)): + Inventory.add_item(str(entry.get("gives", "")), 1) + if str(entry.get("gives", "")) == "sunglasses": + Achievements.unlock("cool_sunglasses") + purchase_made.emit(entry_id) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("shop_joke_free"), 3.0) + GameState.save_game() + return true + if bool(entry.get("joke_continue", false)): + ## „kostet 10 Punkte“ → Spaß, du darfst weiter + var ui2 := get_node_or_null("/root/UIRoot") + if ui2 and ui2.has_method("show_dialog"): + ui2.show_dialog(Locale.tr_key("shop_joke_continue"), 4.0) + GameState.set_flag("paywall_cleared", true) + purchase_made.emit(entry_id) + GameState.save_game() + return true var cost := int(entry.get("cost", 0)) if dressler_points < cost: return false @@ -34,8 +53,8 @@ func purchase(entry_id: String) -> bool: var gives := str(entry.get("gives", "")) if gives != "": Inventory.add_item(gives, 1) - if gives == "sunglasses": - Achievements.unlock("cool_sunglasses") + if bool(entry.get("unlock_next", false)): + GameState.set_flag("paywall_cleared", true) points_changed.emit(dressler_points) purchase_made.emit(entry_id) GameState.save_game() diff --git a/scripts/billboard/billboard_sprite.gd b/scripts/billboard/billboard_sprite.gd index 8f7a46f..b9cef2b 100644 --- a/scripts/billboard/billboard_sprite.gd +++ b/scripts/billboard/billboard_sprite.gd @@ -1,7 +1,7 @@ extends Node3D class_name BillboardSprite -## 2D sprite on a 3D plane that always faces the camera (Mario-tree style). -## TODO: Swap placeholder textures for real pixel art. +## Mario-tree billboard: upright Quad (perpendicular to ground), yaw-only toward camera. +## PlaneMesh defaults to flat on XZ — that made sprites look like floor stickers. @export var texture: Texture2D @export var pixel_size: float = 0.05 @@ -9,9 +9,14 @@ class_name BillboardSprite @onready var _mesh_instance: MeshInstance3D = $MeshInstance3D +var _quad_height: float = 1.0 + func _ready() -> void: _apply() +func _process(_delta: float) -> void: + _face_camera_yaw_only() + func set_texture(tex: Texture2D) -> void: texture = tex _apply() @@ -19,19 +24,41 @@ func set_texture(tex: Texture2D) -> void: func _apply() -> void: if _mesh_instance == null: return - var plane := PlaneMesh.new() - plane.size = Vector2(1.0, 1.0) - _mesh_instance.mesh = plane + # QuadMesh is vertical (XY), facing +Z — correct default for standing sprites. + var quad := QuadMesh.new() + var w := 1.0 + var h := 1.0 + if texture != null: + w = float(texture.get_width()) * pixel_size + h = float(texture.get_height()) * pixel_size + quad.size = Vector2(w, h) + _quad_height = h + _mesh_instance.mesh = quad + # Pivot at feet: lift mesh so bottom sits on this node's origin. + _mesh_instance.position = Vector3(0.0, h * 0.5, 0.0) + _mesh_instance.rotation = Vector3.ZERO + var mat := StandardMaterial3D.new() - mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED + # No engine billboard — we yaw the Node3D ourselves so orientation stays upright. + mat.billboard_mode = BaseMaterial3D.BILLBOARD_DISABLED mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR mat.alpha_scissor_threshold = 0.1 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED # visible from both sides mat.albedo_color = modulate_color if texture != null: mat.albedo_texture = texture - var w := float(texture.get_width()) * pixel_size - var h := float(texture.get_height()) * pixel_size - plane.size = Vector2(w, h) _mesh_instance.material_override = mat + +func _face_camera_yaw_only() -> void: + var cam := get_viewport().get_camera_3d() + if cam == null: + return + var to_cam := cam.global_position - global_position + to_cam.y = 0.0 + if to_cam.length_squared() < 0.0001: + return + # QuadMesh faces +Z; look_at aims -Z at the target — aim -Z away from camera + # so +Z faces the camera. + look_at(global_position - to_cam, Vector3.UP) diff --git a/scripts/combat/cheese_projectile.gd b/scripts/combat/cheese_projectile.gd new file mode 100644 index 0000000..f31b78b --- /dev/null +++ b/scripts/combat/cheese_projectile.gd @@ -0,0 +1,40 @@ +extends Area3D +class_name CheeseProjectile +## Moldy cheese shot at the player. Attack the cheese (not the seller) to destroy it. + +var velocity: Vector3 = Vector3.ZERO +var lifetime: float = 5.0 +var hp: int = 1 + +func _ready() -> void: + collision_layer = 4 + collision_mask = 2 + monitoring = true + body_entered.connect(_on_body) + add_to_group("enemy") + add_to_group("cheese_projectile") + var bill := get_node_or_null("BillboardSprite") + if bill and bill.has_method("set_texture"): + var tex: Texture2D = load("res://assets/sprites/placeholders/cheese.png") + if tex: + bill.set_texture(tex) + +func _physics_process(delta: float) -> void: + global_position += velocity * delta + lifetime -= delta + if lifetime <= 0.0: + call_deferred("queue_free") + +func take_hit(_amount: int = 1) -> void: + hp -= 1 + AudioDirector.play_hit() + if hp <= 0: + var seller = get_meta("seller", null) + if seller != null and is_instance_valid(seller) and seller.has_method("notify_cheese_destroyed"): + seller.notify_cheese_destroyed() + call_deferred("queue_free") + +func _on_body(body: Node3D) -> void: + if body.is_in_group("player"): + GameState.die("death_cheese") + call_deferred("queue_free") diff --git a/scripts/combat/cheese_seller.gd b/scripts/combat/cheese_seller.gd new file mode 100644 index 0000000..71d9d63 --- /dev/null +++ b/scripts/combat/cheese_seller.gd @@ -0,0 +1,67 @@ +extends Node3D +class_name CheeseSeller +## Places a cheese table and periodically launches moldy cheese at the player. +## DESIGN: stab the cheese (projectile), not the seller — seller dies of grief when cheese destroyed. + +@export var texture_path: String = "res://assets/sprites/placeholders/cheese.png" +@export var fire_interval: float = 2.8 +@export var cheese_speed: float = 7.0 + +var _alive: bool = true +var _timer: float = 0.0 +var _grief: int = 0 + +@onready var _billboard: BillboardSprite = $BillboardSprite + +func _ready() -> void: + add_to_group("interactable") + add_to_group("cheese_seller") + _timer = randf() * fire_interval + if _billboard: + var tex: Texture2D = load(texture_path) + if tex: + _billboard.set_texture(tex) + +func _process(delta: float) -> void: + if not _alive: + return + _timer -= delta + if _timer <= 0.0: + _timer = fire_interval + _fire() + +func _fire() -> void: + var player := get_tree().get_first_node_in_group("player") + if player == null: + return + var proj_scene: PackedScene = load("res://scenes/prefabs/cheese_projectile.tscn") + if proj_scene == null: + return + var proj: Node3D = proj_scene.instantiate() + get_tree().current_scene.add_child(proj) + proj.global_position = global_position + Vector3(0, 0.8, 0) + var dir: Vector3 = (player.global_position + Vector3(0, 0.8, 0) - proj.global_position).normalized() + proj.set("velocity", dir * cheese_speed) + proj.add_to_group("cheese_projectile") + proj.add_to_group("enemy") + proj.set_meta("seller", self) + +func notify_cheese_destroyed() -> void: + _grief += 1 + if _grief >= 2: + _alive = false + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_cheese")) + queue_free() + +func on_interact(_player: Node) -> void: + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_cheese")) + +func take_hit(_amount: int = 1) -> void: + ## Hitting the seller does nothing useful (DESIGN). + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_cheese")) diff --git a/scripts/levels/catscene.gd b/scripts/levels/catscene.gd new file mode 100644 index 0000000..46b343a --- /dev/null +++ b/scripts/levels/catscene.gd @@ -0,0 +1,21 @@ +extends Control +## Catscene: open-world joke. Skipping resets the run. + +@onready var label: Label = $Label +@onready var skip_btn: Button = $SkipButton +@onready var cont_btn: Button = $ContinueButton + +func _ready() -> void: + GameState.current_scene_id = "catscene" + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + label.text = Locale.tr_key("dialog_catscene") + skip_btn.text = "Skip Katzin / Catscene" + cont_btn.text = Locale.tr_key("menu_continue") + +func _on_continue_pressed() -> void: + GameState.goto_scene("farm_path") + +func _on_skip_pressed() -> void: + label.text = Locale.tr_key("dialog_catscene_skip") + await get_tree().create_timer(2.0).timeout + GameState.reset_to_intro() diff --git a/scripts/levels/death_screen.gd b/scripts/levels/death_screen.gd index 518f8f2..c66fb99 100644 --- a/scripts/levels/death_screen.gd +++ b/scripts/levels/death_screen.gd @@ -1,17 +1,28 @@ extends Control -## Blinky death screen with cause text + skull every 10s. -## TODO: Final flashy death art. +## Blinky death screen + absurd revive (Nein=revive, Ja=quit). @onready var title: Label = $Title @onready var cause: Label = $Cause -@onready var cont: Button = $ContinueButton +@onready var btn_revive: Button = $ReviveButton +@onready var btn_no: Button = $NoButton +@onready var btn_yes: Button = $YesButton +@onready var hint: Label = $Hint @onready var skull: ColorRect = $Skull +@onready var bg: ColorRect = $Bg var _blink: float = 0.0 var _skull_timer: float = 0.0 +var _colors: Array[Color] = [ + Color(0.05, 0.05, 0.05), + Color(0.9, 0.1, 0.4), + Color(0.1, 0.8, 0.3), + Color(0.2, 0.3, 1.0), + Color(1.0, 0.8, 0.1), +] func _ready() -> void: GameState.current_scene_id = "death_screen" + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _refresh() Locale.locale_changed.connect(_refresh) skull.visible = false @@ -19,11 +30,15 @@ func _ready() -> void: func _refresh() -> void: title.text = Locale.tr_key("death_title") cause.text = Locale.tr_key(GameState.last_death_cause_key) - cont.text = Locale.tr_key("death_continue") + btn_revive.text = Locale.tr_key("death_revive") + btn_no.text = Locale.tr_key("death_no") + btn_yes.text = Locale.tr_key("death_yes") + hint.text = Locale.tr_key("death_no_hint") + "\n" + Locale.tr_key("death_yes_hint") func _process(delta: float) -> void: _blink += delta - modulate = Color(1, 0.4 + 0.6 * abs(sin(_blink * 6.0)), 0.4 + 0.6 * abs(sin(_blink * 4.0))) + var idx := int(_blink) % _colors.size() + bg.color = _colors[idx] _skull_timer += delta if _skull_timer >= 10.0: _skull_timer = 0.0 @@ -31,6 +46,13 @@ func _process(delta: float) -> void: await get_tree().create_timer(1.0).timeout skull.visible = false -func _on_continue_pressed() -> void: - GameState.heal_full() - GameState.goto_scene("title_screen") +func _on_revive_pressed() -> void: + GameState.revive_continue() + +func _on_no_pressed() -> void: + ## Satire: Nein = trotzdem wiederbeleben + GameState.revive_continue() + +func _on_yes_pressed() -> void: + ## Satire: Ja = Spiel beenden → Title + GameState.quit_to_title() diff --git a/scripts/levels/dressler_boss.gd b/scripts/levels/dressler_boss.gd new file mode 100644 index 0000000..f050ef3 --- /dev/null +++ b/scripts/levels/dressler_boss.gd @@ -0,0 +1,63 @@ +extends Node3D +## Dresslermeister: Augen öffnen, Mähdrescher hijacken, Apfel dreschen. + +var _hijacked: int = 0 + +func _ready() -> void: + GameState.current_scene_id = "dressler_boss" + GameState.heal_full() + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_dressler")) + var dressler := get_node_or_null("Dressler") + if dressler and dressler.has_signal("defeated"): + dressler.defeated.connect(_on_dressler_defeated) + # Convert mower hazards into hijackables + for name in ["Mower1", "Mower2"]: + var m := get_node_or_null(name) + if m: + _make_hijackable(m) + +func _make_hijackable(mower: Node) -> void: + mower.set_meta("hijacked", false) + if mower.has_signal("body_entered"): + # Keep death if not hijacked — wrap + pass + # Add interactable proxy as child Area isn't enough; use nearby attack + mower.add_to_group("hijackable_mower") + +func _physics_process(_delta: float) -> void: + if Input.is_action_just_pressed("interact") or Input.is_action_just_pressed("attack"): + _try_hijack() + +func _try_hijack() -> void: + var player := get_tree().get_first_node_in_group("player") + if player == null: + return + for mower in get_tree().get_nodes_in_group("hijackable_mower"): + if mower is Node3D and player.global_position.distance_to(mower.global_position) <= 2.5: + if bool(mower.get_meta("hijacked", false)): + continue + mower.set_meta("hijacked", true) + _hijacked += 1 + # Disable kill: remove from monitoring / free hazard script effect + if mower.has_method("set"): + mower.set("monitoring", false) + if mower is Area3D: + (mower as Area3D).monitoring = false + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog("Fahrer rausgeworfen! Dresche den Apfel! (%d/2)" % _hijacked) + # Bonus damage to dressler for each hijack + var dressler := get_node_or_null("Dressler") + if dressler and dressler.has_method("take_hit"): + dressler.take_hit(3) + return + +func _on_dressler_defeated() -> void: + Achievements.unlock("dressler_apples") + Inventory.add_item("dressler_apple", 1) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_dressler_drop"), 4.0) + GameState.set_flag("dressler_defeated", true) diff --git a/scripts/levels/end_screen.gd b/scripts/levels/end_screen.gd index 400c527..7306e94 100644 --- a/scripts/levels/end_screen.gd +++ b/scripts/levels/end_screen.gd @@ -1,8 +1,52 @@ extends Node3D -## End village with house-build stub. +## Enddorf: Hausbau + Zahnionär. Kein Happy End. func _ready() -> void: GameState.current_scene_id = "end_screen" GameState.heal_full() - if GameState.get_flag("house_built", false) or GameState.house_built: - GameState.house_built = true + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_end_angry"), 4.0) + var house := get_node_or_null("HouseSpot") + if house and house.has_signal("interacted"): + house.interacted.connect(_on_house) + var dentist := get_node_or_null("DentistHeist") + if dentist and dentist.has_signal("interacted"): + dentist.interacted.connect(_on_heist) + if GameState.house_built or GameState.get_flag("house_built", false): + _show_house() + +func _on_house(_n: Node = null) -> void: + GameState.house_built = true + GameState.set_flag("house_built", true) + _show_house() + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("build_house")) + +func _show_house() -> void: + var house := get_node_or_null("BuiltHouse") + if house: + house.visible = true + return + var scene: PackedScene = load("res://scenes/prefabs/npc.tscn") + if scene == null: + return + var h: Node3D = scene.instantiate() + h.name = "BuiltHouse" + add_child(h) + h.global_position = Vector3(3, 0.1, -2) + h.set("texture_path", "res://assets/sprites/placeholders/house.png") + h.set("dialog_key", "build_house") + var bill = h.get_node_or_null("BillboardSprite") + if bill and bill.has_method("set_texture"): + var tex: Texture2D = load("res://assets/sprites/placeholders/house.png") + if tex: + bill.set_texture(tex) + +func _on_heist(_n: Node = null) -> void: + Inventory.add_item("tooth", 100) + Achievements.unlock("tooth_millionaire") + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_toothionaire"), 4.0) diff --git a/scripts/levels/farm.gd b/scripts/levels/farm.gd index 61ceeb7..7a6292f 100644 --- a/scripts/levels/farm.gd +++ b/scripts/levels/farm.gd @@ -1,7 +1,81 @@ extends Node3D -## Level stub: farm -## TODO: Replace placeholders with final pixel art / audio. +## Bayerischer Hof: 3 Quests → Paywall-Joke → Exit zu Dressler. Urhahn-Schock. + +var _fed_pigs: bool = false +var _milked: bool = false +var _chickens: bool = false func _ready() -> void: GameState.current_scene_id = "farm" GameState.heal_full() + _fed_pigs = bool(GameState.get_flag("pigs_fed", false)) + _milked = bool(GameState.get_flag("cows_milked", false)) + _chickens = bool(GameState.get_flag("chickens_taken", false)) + var mood := str(GameState.get_flag("farmer_mood", "direct")) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + if mood == "detour": + ui.show_dialog(Locale.tr_key("dialog_farmer_detour"), 4.0) + else: + ui.show_dialog(Locale.tr_key("dialog_farmer_direct"), 3.5) + await get_tree().create_timer(1.2).timeout + ui.show_dialog(Locale.tr_key("dialog_farmer_quest"), 4.0) + var urhahn := get_node_or_null("Urhahn") + if urhahn and urhahn.has_signal("defeated"): + urhahn.defeated.connect(_on_urhahn_down) + var hazard := get_node_or_null("RoosterHazard") + if hazard and hazard.has_signal("body_entered"): + # Extra: shock before death for flavor when contacting hazard + if not hazard.body_entered.is_connected(_on_rooster_touch): + hazard.body_entered.connect(_on_rooster_touch) + var exit := get_node_or_null("Exit") + if exit: + exit.set("require_flag", "paywall_cleared") + var ticket := get_node_or_null("TicketVendor") + if ticket and ticket.has_signal("interacted"): + ticket.interacted.connect(_on_ticket) + +func _physics_process(_delta: float) -> void: + if GameState.get_flag("pigs_fed", false) and not _fed_pigs: + complete_pig() + if GameState.get_flag("cows_milked", false) and not _milked: + complete_cow() + if GameState.get_flag("chickens_taken", false) and not _chickens: + complete_chicken() + +func complete_pig() -> void: + _fed_pigs = true + _check_quests() + +func complete_cow() -> void: + _milked = true + _check_quests() + +func complete_chicken() -> void: + _chickens = true + if not Inventory.has_item("lava_chicken"): + Inventory.add_item("lava_chicken", 1) + _check_quests() + +func _check_quests() -> void: + if not (_fed_pigs and _milked and _chickens): + return + GameState.set_flag("farm_quests_done", true) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_paywall"), 4.0) + Shop.purchase("continue_joke") + GameState.set_flag("paywall_cleared", true) + +func _on_urhahn_down() -> void: + GameState.set_flag("urhahn_defeated", true) + +func _on_rooster_touch(body: Node3D) -> void: + if body.is_in_group("player") and not GameState.get_flag("urhahn_defeated", false): + GameState.apply_shock(10.0) + +func _on_ticket(_n: Node = null) -> void: + Inventory.add_item("death_ticket", 1) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("item_death_ticket")) diff --git a/scripts/levels/farm_path.gd b/scripts/levels/farm_path.gd index 1ce08c1..d60f1a7 100644 --- a/scripts/levels/farm_path.gd +++ b/scripts/levels/farm_path.gd @@ -1,7 +1,52 @@ extends Node3D -## Level stub: farm_path -## TODO: Replace placeholders with final pixel art / audio. +## Enger Weg + Gabelung. Rechts = Magier (Knockback / Planet destroy). + +var _went_right: bool = false func _ready() -> void: GameState.current_scene_id = "farm_path" GameState.heal_full() + _went_right = bool(GameState.get_flag("went_right_first", false)) + var left := get_node_or_null("LeftExit") + if left: + left.set("use_goto_next", false) + left.set("next_scene_id", "") + if left.has_signal("body_entered") and not left.body_entered.is_connected(_on_left_body): + left.body_entered.connect(_on_left_body) + var right := get_node_or_null("RightZone") + if right: + right.set("use_goto_next", false) + right.set("next_scene_id", "") + if right.has_signal("body_entered") and not right.body_entered.is_connected(_on_right_body): + right.body_entered.connect(_on_right_body) + +func _on_left_body(body: Node3D) -> void: + if not body.is_in_group("player"): + return + if _went_right or GameState.get_flag("went_right_first", false): + GameState.set_flag("farmer_mood", "detour") + else: + GameState.set_flag("farmer_mood", "direct") + GameState.set_flag("planet_destroyed", false) + GameState.goto_scene("farm") + +func _on_right_body(body: Node3D) -> void: + if not body.is_in_group("player"): + return + _went_right = true + GameState.set_flag("went_right_first", true) + var ui := get_node_or_null("/root/UIRoot") + # Knockback toward left path + if body is CharacterBody3D: + body.global_position += Vector3(-4, 0, 0) + if GameState.get_flag("called_black_mage", false): + GameState.set_flag("mage_angered", true) + GameState.set_flag("planet_destroyed", true) + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_mage_angry"), 4.0) + await get_tree().create_timer(2.0).timeout + GameState.goto_scene("moon") + else: + GameState.set_flag("called_black_mage", true) + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_mage_push")) diff --git a/scripts/levels/final_boss.gd b/scripts/levels/final_boss.gd index 8391dec..0ca6310 100644 --- a/scripts/levels/final_boss.gd +++ b/scripts/levels/final_boss.gd @@ -1,9 +1,91 @@ extends Node3D -## Hongkong / Glitschkönig final boss. +## Glitschkönig: Reveal, Quirxels, Split-Phasen, Flucht des letzten Pixels. + +enum Phase { FULL, SPLIT, FLEE, DONE } + +var _phase: Phase = Phase.FULL +var _revealed: bool = false +var _splits_left: int = 2 func _ready() -> void: GameState.current_scene_id = "final_boss" GameState.heal_full() + _revealed = bool(GameState.get_flag("glitsch_revealed", false)) + if not _revealed: + _do_reveal() + var boss := get_node_or_null("Hongkong") + if boss: + boss.set("max_hp", 12) + if boss.has_signal("hit"): + boss.hit.connect(_on_boss_hit) + if boss.has_signal("defeated"): + boss.defeated.connect(_on_boss_defeated) + var t := Timer.new() + t.wait_time = 2.8 + t.autostart = true + t.timeout.connect(_spawn_quirxel) + add_child(t) + +func _do_reveal() -> void: + GameState.set_flag("glitsch_revealed", true) + Inventory.add_item("banana", 1) + Inventory.add_item("wartchen", 1) var ui := get_node_or_null("/root/UIRoot") if ui and ui.has_method("show_dialog"): - ui.show_dialog(Locale.tr_key("dialog_hongkong_boss")) + ui.show_dialog(Locale.tr_key("dialog_reveal"), 5.5) + +func _on_boss_hit(remaining: int) -> void: + if _phase == Phase.FULL and remaining <= 6 and _splits_left > 0: + _phase = Phase.SPLIT + _splits_left -= 1 + _spawn_split_clone() + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog("Er teilt sich wie Minecraft-Schleim!", 2.5) + elif remaining <= 2 and _phase != Phase.FLEE and _phase != Phase.DONE: + _phase = Phase.FLEE + var boss := get_node_or_null("Hongkong") + if boss is Node3D: + boss.global_position += Vector3(randf_range(-4, 4), 0, -3) + +func _spawn_split_clone() -> void: + var scene: PackedScene = load("res://scenes/prefabs/enemy.tscn") + if scene == null: + return + var c: Node3D = scene.instantiate() + c.name = "GlitschSplit" + add_child(c) + c.global_position = Vector3(randf_range(-3, 3), 0.1, -2) + c.set("max_hp", 4) + c.set("texture_path", "res://assets/sprites/placeholders/hongkong.png") + var bill = c.get_node_or_null("BillboardSprite") + if bill and bill.has_method("set_texture"): + var tex: Texture2D = load("res://assets/sprites/placeholders/hongkong.png") + if tex: + bill.set_texture(tex) + +func _spawn_quirxel() -> void: + if _phase == Phase.DONE or GameState.get_flag("hongkong_defeated", false): + return + var scene: PackedScene = load("res://scenes/prefabs/hazard.tscn") + if scene == null: + return + var h: Node3D = scene.instantiate() + add_child(h) + h.global_position = Vector3(randf_range(-6, 6), 0.1, randf_range(-6, 0)) + h.set("death_cause_key", "death_quirxel") + await get_tree().create_timer(5.0).timeout + if is_instance_valid(h): + h.queue_free() + +func _on_boss_defeated() -> void: + _phase = Phase.DONE + Achievements.unlock("beat_hongkong") + GameState.set_flag("hongkong_defeated", true) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_hongkong_boss"), 3.0) + await get_tree().create_timer(2.5).timeout + ui.show_dialog(Locale.tr_key("dialog_bear_end"), 5.0) + await get_tree().create_timer(3.5).timeout + GameState.goto_scene("end_screen") diff --git a/scripts/levels/intro_cutscene.gd b/scripts/levels/intro_cutscene.gd index e03964d..511c2d4 100644 --- a/scripts/levels/intro_cutscene.gd +++ b/scripts/levels/intro_cutscene.gd @@ -1,6 +1,5 @@ extends Control -## Intro: desk + console + explosion. -## TODO: Real cutscene art and epic narrator VO. +## Intro: Tisch, Konsole, Sprecher, Explosion. @onready var label: Label = $Label @@ -8,6 +7,7 @@ var _step: int = 0 func _ready() -> void: GameState.current_scene_id = "intro_cutscene" + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE AudioDirector.stop_music() _show_step() @@ -20,10 +20,10 @@ func _show_step() -> void: 0: label.text = Locale.tr_key("dialog_intro_1") 1: - label.text = Locale.tr_key("dialog_intro_2") - 2: AudioDirector.play_explosion() label.text = "BOOM" + 2: + label.text = Locale.tr_key("dialog_intro_2") _: GameState.goto_scene("village") diff --git a/scripts/levels/massage_studio.gd b/scripts/levels/massage_studio.gd index bd23879..88170d8 100644 --- a/scripts/levels/massage_studio.gd +++ b/scripts/levels/massage_studio.gd @@ -1,7 +1,53 @@ extends Node3D -## Level stub: massage_studio -## TODO: Replace placeholders with final pixel art / audio. +## Massage: Schlange massieren → Bear-Save; oder Secret-Milch. + +@onready var choice_ui: CanvasLayer = $ChoiceUI func _ready() -> void: GameState.current_scene_id = "massage_studio" GameState.heal_full() + choice_ui.visible = false + var cow := get_node_or_null("CowMasseur") + if cow and cow.has_signal("interacted"): + cow.interacted.connect(_on_cow) + if cow: + cow.require_item_id = "" + cow.achievement_id = "" + cow.quest_flag = "" + cow.consume_required = false + cow.give_item_id = "" + +func _on_cow(_npc: Node = null) -> void: + choice_ui.visible = true + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + $ChoiceUI/Panel/Margin/VBox/Label.text = Locale.tr_key("dialog_cow") + $ChoiceUI/Panel/Margin/VBox/MassageBtn.text = Locale.tr_key("choice_massage_now") + $ChoiceUI/Panel/Margin/VBox/DrinkBtn.text = Locale.tr_key("choice_drink_first") + +func _close_choices() -> void: + choice_ui.visible = false + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + +func _on_massage_pressed() -> void: + _close_choices() + if not Inventory.has_item("snake"): + var ui0 := get_node_or_null("/root/UIRoot") + if ui0 and ui0.has_method("show_dialog"): + ui0.show_dialog(Locale.tr_key("dialog_villager_snake")) + return + Inventory.remove_item("snake", 1) + GameState.set_flag("snake_massaged", true) + Achievements.unlock("snake_massage") + if not GameState.get_flag("bear_saved", false): + Inventory.add_item("tooth", 1) + GameState.set_flag("bear_saved", true) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_cow_massage") + "\n" + Locale.tr_key("dialog_bear_saved"), 5.0) + +func _on_drink_pressed() -> void: + _close_choices() + Inventory.add_item("milk", 1) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_cow_drink"), 4.0) diff --git a/scripts/levels/meadow.gd b/scripts/levels/meadow.gd index 4d529ea..2928a91 100644 --- a/scripts/levels/meadow.gd +++ b/scripts/levels/meadow.gd @@ -1,7 +1,129 @@ extends Node3D -## Level stub: meadow -## TODO: Replace placeholders with final pixel art / audio. +## Wiese: Eisbär retten (Schlangen holen → Massage ODER hier abgeben), Glitzos. + +var _glitzo_timer: float = 0.0 +var _bear_saved: bool = false +var _attack_cd: float = 0.0 +var _first_glitzo_done: bool = false func _ready() -> void: GameState.current_scene_id = "meadow" GameState.heal_full() + _bear_saved = bool(GameState.get_flag("bear_saved", false)) + _first_glitzo_done = Achievements.is_unlocked("first_glitzo") + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + if _bear_saved: + ui.show_dialog(Locale.tr_key("dialog_bear_saved")) + else: + ui.show_dialog(Locale.tr_key("dialog_bear_help")) + var bear := get_node_or_null("Bear") + if bear and bear.has_signal("interacted"): + bear.interacted.connect(_on_bear) + var snake := get_node_or_null("SnakePickup") + if snake and snake.has_signal("interacted"): + snake.interacted.connect(_on_snake_pickup) + # Disable mower kill if snakes already bagged? Keep hazard until massage done. + if GameState.get_flag("snake_massaged", false): + _disable_mower() + +func _process(delta: float) -> void: + _glitzo_timer += delta + if _glitzo_timer >= 3.5: + _glitzo_timer = 0.0 + _spawn_glitzo() + _tick_glitzo_attacks(delta) + _cull_glitzos_off_meadow() + +func _tick_glitzo_attacks(delta: float) -> void: + _attack_cd = max(0.0, _attack_cd - delta) + if _attack_cd > 0.0: + return + var player := get_tree().get_first_node_in_group("player") + if player == null: + return + for child in get_children(): + if not child.is_in_group("enemy"): + continue + if not str(child.get("texture_path")).ends_with("glitzo.png") and not child.name.begins_with("Glitzo"): + continue + if child is Node3D and player.global_position.distance_to(child.global_position) < 1.6: + _attack_cd = 1.15 + GameState.register_glitzo_hit() + return + +func _cull_glitzos_off_meadow() -> void: + ## DESIGN: leave the meadow → they shed pixels and die. + for child in get_children(): + if child is Node3D and (child.name.begins_with("Glitzo") or str(child.get("texture_path")).ends_with("glitzo.png")): + var p: Vector3 = child.global_position + if abs(p.x) > 18.0 or abs(p.z) > 18.0: + child.queue_free() + +func _spawn_glitzo() -> void: + var scene: PackedScene = load("res://scenes/prefabs/enemy.tscn") + if scene == null: + return + var g: Node3D = scene.instantiate() + g.name = "GlitzoSpawn_%d" % Time.get_ticks_msec() + g.set("max_hp", 4) + g.set("texture_path", "res://assets/sprites/placeholders/glitzo.png") + if not _first_glitzo_done: + g.set("achievement_on_defeat", "first_glitzo") + add_child(g) + g.global_position = Vector3(randf_range(-8, 8), 0.1, randf_range(-6, 2)) + var bill: Node = g.get_node_or_null("BillboardSprite") + if bill and bill.has_method("set_texture"): + var tex: Texture2D = load("res://assets/sprites/placeholders/glitzo.png") + if tex: + bill.set_texture(tex) + if g.has_signal("defeated"): + g.defeated.connect(_on_glitzo_killed) + +func _on_glitzo_killed() -> void: + _first_glitzo_done = true + +func _on_bear(_npc: Node = null) -> void: + var ui := get_node_or_null("/root/UIRoot") + if _bear_saved: + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_bear_saved")) + return + # Need snakes rescued via inventory + preferably massaged; tooth if massaged or handing snakes in after pickup + if Inventory.has_item("snake") and GameState.get_flag("snake_massaged", false): + _save_bear() + return + if Inventory.has_item("snake"): + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_villager_snake")) + return + # First talk: point at snakes / give hint, auto-pickup one for convenience + Inventory.add_item("snake", 1) + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_snake_pain") + "\n" + Locale.tr_key("dialog_bear_help")) + +func _on_snake_pickup(_npc: Node = null) -> void: + if not Inventory.has_item("snake"): + Inventory.add_item("snake", 1) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_snake_pain")) + +func _save_bear() -> void: + _bear_saved = true + GameState.set_flag("bear_saved", true) + if not Inventory.has_item("tooth"): + Inventory.add_item("tooth", 1) + _disable_mower() + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_bear_saved"), 4.5) + +func _disable_mower() -> void: + var mower := get_node_or_null("MowerHazard") + if mower: + mower.queue_free() + +func notify_snake_massaged() -> void: + if not _bear_saved: + _save_bear() diff --git a/scripts/levels/moon.gd b/scripts/levels/moon.gd index 6844fe8..cd47f98 100644 --- a/scripts/levels/moon.gd +++ b/scripts/levels/moon.gd @@ -1,7 +1,91 @@ extends Node3D -## Moon cheese level. +## Mond: Käsekatapulte, Schwert, Sonnenbrille, Fake-Bowser Hammer-Fight. + +@onready var hammer_ui: CanvasLayer = $HammerUI +var _correct_hammer: int = 1 +var _hits_needed: int = 3 +var _hits: int = 0 func _ready() -> void: GameState.current_scene_id = "moon" GameState.heal_full() Achievements.unlock("moon_cheese") + hammer_ui.visible = false + _spawn_cheese_sellers() + var bowser := get_node_or_null("FakeBowser") + if bowser and bowser.has_signal("interacted"): + bowser.interacted.connect(_start_bowser_fight) + var sword := get_node_or_null("SwordStone") + if sword: + sword.give_item_id = "moon_sword" + sword.dialog_key = "dialog_cheese" + var shades := get_node_or_null("MoonResearcher") + if shades and shades.has_signal("interacted"): + shades.interacted.connect(_on_shades) + # Convert old CheeseSeller NPC if present into shooter + var old := get_node_or_null("CheeseSeller") + if old and not old.has_method("notify_cheese_destroyed"): + old.queue_free() + +func _spawn_cheese_sellers() -> void: + var scene: PackedScene = load("res://scenes/prefabs/cheese_seller.tscn") + if scene == null: + return + for i in range(4): + var s: Node3D = scene.instantiate() + add_child(s) + s.global_position = Vector3(-6.0 + i * 4.0, 0.1, -2.0 - (i % 2) * 2.0) + +func _on_shades(_n: Node = null) -> void: + Shop.purchase("sunglasses") + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_sunglasses_free")) + +func _start_bowser_fight(_n: Node = null) -> void: + if GameState.get_flag("bowser_defeated", false): + Inventory.add_item("wedding_egg", 1) + return + hammer_ui.visible = true + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _hits = 0 + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_bowser")) + _refresh_hammers() + +func _refresh_hammers() -> void: + _correct_hammer = randi_range(1, 3) + $HammerUI/Panel/Margin/VBox/Label.text = Locale.tr_key("dialog_bowser") + " [%d/%d]" % [_hits, _hits_needed] + $HammerUI/Panel/Margin/VBox/H1.text = Locale.tr_key("choice_hammer_1") + $HammerUI/Panel/Margin/VBox/H2.text = Locale.tr_key("choice_hammer_2") + $HammerUI/Panel/Margin/VBox/H3.text = Locale.tr_key("choice_hammer_3") + +func _pick(hammer: int) -> void: + if hammer == _correct_hammer: + _hits += 1 + AudioDirector.play_hit() + if _hits >= _hits_needed: + hammer_ui.visible = false + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + GameState.set_flag("bowser_defeated", true) + Achievements.unlock("beat_bowser") + Inventory.add_item("chicken", 1) + Inventory.add_item("wedding_egg", 1) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog("Hochzeitsei! Im Inventar benutzen = Whiff zum Endboss.", 4.0) + else: + _refresh_hammers() + else: + hammer_ui.visible = false + GameState.die("death_bowser") + +func _on_h1() -> void: + _pick(1) + +func _on_h2() -> void: + _pick(2) + +func _on_h3() -> void: + _pick(3) diff --git a/scripts/levels/shoe_boss.gd b/scripts/levels/shoe_boss.gd index 8447af2..42b9a73 100644 --- a/scripts/levels/shoe_boss.gd +++ b/scripts/levels/shoe_boss.gd @@ -1,5 +1,5 @@ extends Node3D -## Schuhmacher Reif boss under the table. +## Schuhmacher Reif. Sieg → Tonne Tiramisu + Fett-Debuff. func _ready() -> void: GameState.current_scene_id = "shoe_boss" @@ -7,3 +7,14 @@ func _ready() -> void: var ui := get_node_or_null("/root/UIRoot") if ui and ui.has_method("show_dialog"): ui.show_dialog(Locale.tr_key("dialog_reif")) + var reif := get_node_or_null("Reif") + if reif and reif.has_signal("defeated"): + reif.defeated.connect(_on_reif_defeated) + +func _on_reif_defeated() -> void: + Inventory.add_item("tiramisu", 1000) + GameState.apply_fat(600.0) + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_tiramisu"), 5.0) + GameState.set_flag("reif_defeated", true) diff --git a/scripts/levels/title_screen.gd b/scripts/levels/title_screen.gd index bc8dee4..85f3bdd 100644 --- a/scripts/levels/title_screen.gd +++ b/scripts/levels/title_screen.gd @@ -1,26 +1,35 @@ extends Control -## Title screen with placeholder rain + loud music. -## TODO: Real title art (Mario/Sonic/Burger rain, disturbed human). +## Title: pixel rain, Pay2Win, loud music, Burger-Cham, first-run language. @onready var subtitle: Label = $Subtitle @onready var bubble: Label = $Bubble @onready var start_btn: Button = $StartButton @onready var rain: Node2D = $Rain +@onready var lang_panel: PanelContainer = $LangPanel +@onready var music_hint: Label = $MusicHint var _rain_sprites: Array[Sprite2D] = [] func _ready() -> void: GameState.current_scene_id = "title_screen" + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE AudioDirector.burger_cham_enabled = true AudioDirector.play_title_music() _refresh() Locale.locale_changed.connect(_refresh) _spawn_rain() + lang_panel.visible = Settings.needs_language_picker() + start_btn.visible = not lang_panel.visible func _refresh() -> void: - subtitle.text = Locale.tr_key("title_subtitle") + subtitle.text = Locale.tr_key("title_pay2win") bubble.text = Locale.tr_key("title_bubble") start_btn.text = Locale.tr_key("menu_start") + music_hint.text = Locale.tr_key("title_music_hint") + $LangPanel/Margin/VBox/Title.text = Locale.tr_key("lang_pick_title") + $LangPanel/Margin/VBox/BtnDE.text = Locale.tr_key("lang_de") + $LangPanel/Margin/VBox/BtnEN.text = Locale.tr_key("lang_en") + $LangPanel/Margin/VBox/BtnBY.text = Locale.tr_key("lang_by") func _spawn_rain() -> void: var paths = [ @@ -28,7 +37,7 @@ func _spawn_rain() -> void: "res://assets/sprites/placeholders/sonic.png", "res://assets/sprites/placeholders/burger.png", ] - for i in range(24): + for i in range(28): var s := Sprite2D.new() s.texture = load(paths[i % paths.size()]) s.position = Vector2(randf() * 1280.0, randf() * -720.0) @@ -39,11 +48,29 @@ func _spawn_rain() -> void: func _process(delta: float) -> void: for s in _rain_sprites: - s.position.y += (80.0 + s.get_index() * 3.0) * delta + s.position.y += (90.0 + s.get_index() * 2.5) * delta if s.position.y > 760.0: s.position.y = -40.0 s.position.x = randf() * 1280.0 +func _pick_lang(lang: String) -> void: + Settings.set_language(lang) + lang_panel.visible = false + start_btn.visible = true + _refresh() + +func _on_lang_de() -> void: + _pick_lang("de") + +func _on_lang_en() -> void: + _pick_lang("en") + +func _on_lang_by() -> void: + _pick_lang("by") + func _on_start_pressed() -> void: + if Settings.needs_language_picker(): + lang_panel.visible = true + return AudioDirector.burger_cham_enabled = false GameState.start_new_game() diff --git a/scripts/levels/village.gd b/scripts/levels/village.gd index cf02b3e..e9620f6 100644 --- a/scripts/levels/village.gd +++ b/scripts/levels/village.gd @@ -1,7 +1,34 @@ extends Node3D -## Level stub: village -## TODO: Replace placeholders with final pixel art / audio. +## Kaputtes AC-Dorf: fliehende NPCs, Tisch-Knopf → Reif. + +var _flee_dirs: Dictionary = {} func _ready() -> void: GameState.current_scene_id = "village" GameState.heal_full() + var ui := get_node_or_null("/root/UIRoot") + if ui and ui.has_method("show_dialog"): + ui.show_dialog(Locale.tr_key("dialog_villager_run")) + # Strip wrong early toothionaire / snake gift + var dentist := get_node_or_null("Dentist") + if dentist: + dentist.give_item_id = "" + dentist.achievement_id = "" + dentist.quest_flag = "" + dentist.dialog_key = "dialog_villager_snake" + var fan := get_node_or_null("NPC_HongkongFan") + if fan: + fan.give_item_id = "" + fan.dialog_key = "dialog_villager_run" + for n in get_children(): + if n.is_in_group("interactable") and n.name != "TableButton" and n is Node3D: + _flee_dirs[n] = Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized() + +func _process(delta: float) -> void: + for n in _flee_dirs.keys(): + if not is_instance_valid(n): + continue + var node: Node3D = n + node.global_position += _flee_dirs[n] * 1.8 * delta + if node.global_position.length() > 22.0: + _flee_dirs[n] = -_flee_dirs[n] diff --git a/scripts/player/player_controller.gd b/scripts/player/player_controller.gd index 62f25b4..3550fdb 100644 --- a/scripts/player/player_controller.gd +++ b/scripts/player/player_controller.gd @@ -1,38 +1,92 @@ extends CharacterBody3D class_name PlayerController -## Simple top-down-ish 3D movement for billboard levels. +## Third-person controller: camera follows player, movement is camera-relative. +## Billboard sprites face the camera (Mario-tree style). @export var move_speed: float = 6.0 @export var attack_range: float = 1.8 @export var attack_damage: int = 1 +@export var mouse_sensitivity: float = 0.0025 +@export var min_pitch_deg: float = -35.0 +@export var max_pitch_deg: float = 55.0 +@export var spring_length: float = 5.5 +@export var camera_height: float = 1.4 signal interacted signal attacked @onready var _billboard: BillboardSprite = $BillboardSprite +@onready var _yaw_pivot: Node3D = $CameraYaw +@onready var _spring: SpringArm3D = $CameraYaw/SpringArm3D +@onready var _camera: Camera3D = $CameraYaw/SpringArm3D/Camera3D var _locked: bool = false +var _pitch: float = deg_to_rad(-18.0) func _ready() -> void: add_to_group("player") + if _spring: + _spring.spring_length = spring_length + _spring.collision_mask = 1 + _spring.position = Vector3(0.0, camera_height, 0.0) + if _camera: + _camera.current = true + _camera.fov = 70.0 + _apply_pitch() + _set_mouse_captured(true) if _billboard and _billboard.texture == null: var tex: Texture2D = load("res://assets/sprites/placeholders/player.png") if tex: _billboard.set_texture(tex) +func _exit_tree() -> void: + _set_mouse_captured(false) + +func _unhandled_input(event: InputEvent) -> void: + if _locked or get_tree().paused: + return + if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + var motion := event as InputEventMouseMotion + _yaw_pivot.rotate_y(-motion.relative.x * mouse_sensitivity) + _pitch = clampf( + _pitch - motion.relative.y * mouse_sensitivity, + deg_to_rad(min_pitch_deg), + deg_to_rad(max_pitch_deg) + ) + _apply_pitch() + elif event is InputEventMouseButton and event.pressed: + _set_mouse_captured(true) + func _physics_process(_delta: float) -> void: if _locked or get_tree().paused: velocity = Vector3.ZERO return + var input := Vector2( Input.get_axis("move_left", "move_right"), Input.get_axis("move_up", "move_down") ) - var dir := Vector3(input.x, 0.0, input.y) + var basis_yaw := _yaw_pivot.global_transform.basis + var forward := -basis_yaw.z + forward.y = 0.0 + forward = forward.normalized() + var right := basis_yaw.x + right.y = 0.0 + right = right.normalized() + var dir := (right * input.x) + (forward * -input.y) if dir.length() > 1.0: dir = dir.normalized() - velocity = dir * move_speed + var speed := move_speed + if GameState.is_fat(): + speed *= 0.15 + elif GameState.is_glitched_slow(): + speed *= 0.55 + if GameState.is_shocked(): + velocity = Vector3.ZERO + return + velocity = dir * speed move_and_slide() + if Input.is_action_just_pressed("interact"): _try_interact() if Input.is_action_just_pressed("attack"): @@ -40,6 +94,28 @@ func _physics_process(_delta: float) -> void: func lock_input(locked: bool) -> void: _locked = locked + if locked: + _set_mouse_captured(false) + elif not get_tree().paused: + _set_mouse_captured(true) + +func set_gameplay_focused(focused: bool) -> void: + ## Called by pause UI: release mouse in menus, recapture in gameplay. + if focused and not _locked: + _set_mouse_captured(true) + else: + _set_mouse_captured(false) + +func _apply_pitch() -> void: + if _spring == null: + return + _spring.rotation.x = _pitch + +func _set_mouse_captured(captured: bool) -> void: + if captured: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + else: + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE func _try_interact() -> void: interacted.emit() @@ -52,7 +128,14 @@ func _try_interact() -> void: func _try_attack() -> void: attacked.emit() AudioDirector.play_hit() + var dmg := attack_damage + if Inventory.has_item("tooth"): + dmg = max(dmg, 2) + if Inventory.has_item("moon_sword"): + dmg = max(dmg, 99) + if Inventory.has_item("wartchen"): + dmg = max(dmg, 3) for body in get_tree().get_nodes_in_group("enemy"): if body is Node3D and global_position.distance_to(body.global_position) <= attack_range: if body.has_method("take_hit"): - body.take_hit(attack_damage) + body.take_hit(dmg) diff --git a/scripts/tools/generate_scaffold.py b/scripts/tools/generate_scaffold.py index 30bb0c4..64cfab7 100644 --- a/scripts/tools/generate_scaffold.py +++ b/scripts/tools/generate_scaffold.py @@ -96,6 +96,9 @@ def player_scene() -> str: radius = 0.35 height = 1.0 +[sub_resource type="SphereShape3D" id="SphereShape3D_spring"] +radius = 0.2 + [node name="Player" type="CharacterBody3D"] collision_layer = 2 collision_mask = 1 @@ -106,7 +109,20 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) shape = SubResource("CapsuleShape3D_1") [node name="BillboardSprite" parent="." instance=ExtResource("2")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) + +[node name="CameraYaw" type="Node3D" parent="."] + +[node name="SpringArm3D" type="SpringArm3D" parent="CameraYaw"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0) +spring_length = 5.5 +collision_mask = 1 +margin = 0.2 +shape = SubResource("SphereShape3D_spring") + +[node name="Camera3D" type="Camera3D" parent="CameraYaw/SpringArm3D"] +current = true +fov = 70.0 """ @@ -120,7 +136,7 @@ def entity_scene(name: str, script: str, extras: str = "") -> str: script = ExtResource("1") {extras} [node name="BillboardSprite" parent="." instance=ExtResource("2")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) """ @@ -373,11 +389,6 @@ script = ExtResource("1") transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0) shadow_enabled = false -[node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12) -current = true -fov = 50.0 - [node name="Ground" type="StaticBody3D" parent="."] collision_layer = 1 diff --git a/scripts/ui/ui_root.gd b/scripts/ui/ui_root.gd index f705339..b91f208 100644 --- a/scripts/ui/ui_root.gd +++ b/scripts/ui/ui_root.gd @@ -13,8 +13,11 @@ extends CanvasLayer @onready var music_slider: HSlider = $PausePanel/Margin/VBox/MusicSlider @onready var lang_option: OptionButton = $PausePanel/Margin/VBox/LangOption @onready var hp_label: Label = $HUD/HpLabel +@onready var fat_stim: ColorRect = $FatStim +@onready var fat_label: Label = $FatStim/FatLabel var _dialog_timer: float = 0.0 +var _fat_blink: float = 0.0 func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS @@ -35,6 +38,7 @@ func _ready() -> void: Inventory.inventory_changed.connect(_refresh_inventory) Shop.points_changed.connect(_refresh_points) GameState.hp_changed.connect(_on_hp) + GameState.status_changed.connect(func(): _on_hp(GameState.hp, GameState.max_hp)) _on_hp(GameState.hp, GameState.max_hp) _refresh_inventory() _refresh_points(Shop.dressler_points) @@ -44,6 +48,17 @@ func _process(delta: float) -> void: _dialog_timer -= delta if _dialog_timer <= 0.0: dialog_panel.visible = false + _update_fat_stim(delta) + +func _update_fat_stim(delta: float) -> void: + if fat_stim == null: + return + var fat := GameState.is_fat() + fat_stim.visible = fat and GameState.current_scene_id not in ["title_screen", "death_screen", "intro_cutscene", "catscene"] + if fat: + _fat_blink += delta + fat_label.text = Locale.tr_key("dialog_tiramisu") + fat_stim.color = Color(0.05, 0.05, 0.08, 0.35 + 0.25 * abs(sin(_fat_blink * 3.0))) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("pause_menu"): @@ -51,14 +66,20 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() func toggle_pause() -> void: - if GameState.current_scene_id in ["title_screen", "death_screen", "intro_cutscene"]: + if GameState.current_scene_id in ["title_screen", "death_screen", "intro_cutscene", "catscene"]: return pause_panel.visible = not pause_panel.visible get_tree().paused = pause_panel.visible + _sync_player_focus(not pause_panel.visible) if pause_panel.visible: _refresh_texts() _refresh_inventory() +func _sync_player_focus(focused: bool) -> void: + for node in get_tree().get_nodes_in_group("player"): + if node.has_method("set_gameplay_focused"): + node.set_gameplay_focused(focused) + func show_dialog(text: String, duration: float = 3.5) -> void: dialog_label.text = text dialog_panel.visible = true @@ -84,8 +105,8 @@ func _refresh_texts() -> void: $PausePanel/Margin/VBox/BuyPointButton.text = Locale.tr_key("menu_buy_point") $PausePanel/Margin/VBox/BuySunglassesButton.text = Locale.tr_key("shop_sunglasses") $PausePanel/Margin/VBox/BuyTicketButton.text = Locale.tr_key("shop_ticket") - $PausePanel/Margin/VBox/BuyMilkButton.text = Locale.tr_key("shop_milk") - $PausePanel/Margin/VBox/BuyTiramisuButton.text = Locale.tr_key("shop_tiramisu") + $PausePanel/Margin/VBox/BuyMilkButton.text = Locale.tr_key("shop_continue_joke") + $PausePanel/Margin/VBox/BuyTiramisuButton.text = Locale.tr_key("shop_next_level") $PausePanel/Margin/VBox/SfxLabel.text = Locale.tr_key("menu_sfx") $PausePanel/Margin/VBox/MusicLabel.text = Locale.tr_key("menu_music") $PausePanel/Margin/VBox/LangLabel.text = Locale.tr_key("menu_language") @@ -105,7 +126,14 @@ func _refresh_inventory() -> void: inventory_list.set_item_metadata(inventory_list.item_count - 1, id) func _on_hp(hp: int, max_hp: int) -> void: - hp_label.text = "HP %d/%d" % [hp, max_hp] + var extra := "" + if GameState.is_fat(): + extra += " | " + Locale.tr_key("status_fat", [str(int(GameState.fat_time_left))]) + if GameState.is_shocked(): + extra += " | " + Locale.tr_key("status_shock") + if GameState.is_glitched_slow(): + extra += " | " + Locale.tr_key("status_glitch") + hp_label.text = "HP %d/%d%s" % [hp, max_hp, extra] func _on_achievement(_id: String, title: String) -> void: toast_label.text = Locale.tr_key("toast_achievement", [title]) @@ -122,6 +150,7 @@ func _on_burger_cham() -> void: func _on_resume_pressed() -> void: pause_panel.visible = false get_tree().paused = false + _sync_player_focus(true) func _on_buy_point() -> void: Shop.purchase("buy_point") @@ -133,10 +162,10 @@ func _on_buy_ticket() -> void: Shop.purchase("death_ticket") func _on_buy_milk() -> void: - Shop.purchase("milk") + Shop.purchase("continue_joke") func _on_buy_tiramisu() -> void: - Shop.purchase("tiramisu") + Shop.purchase("next_level") func _on_sfx_changed(value: float) -> void: Settings.set_sfx_volume(value)