diff --git a/lib/components/baker.dart b/lib/components/baker.dart new file mode 100644 index 0000000..2a9770d --- /dev/null +++ b/lib/components/baker.dart @@ -0,0 +1,61 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:moonshiner_game/components/dialogue.dart'; +import 'package:moonshiner_game/components/npc.dart'; + +class Baker extends AbstractNPC { + Baker({required Vector2 position}) + : super( + npcCharacter: 'Baker', + dialogues: [ + "Fresh bread today!", + "A hard day’s work, but worth it.", + "Care for a slice?", + ], + position: position, + ); + + @override + Color getColorForNPC() => Colors.brown; + + @override + void showDialogue() { + if (messageDisplayed) return; + + final message = dialogues[currentDialogueIndex]; + currentDialogueIndex = (currentDialogueIndex + 1) % dialogues.length; + + final npcDialogue = NPCDialogueComponent( + message: message, + npcColor: getColorForNPC(), + ); + + gameRef.add(npcDialogue); + npcDialogue.showWithTimeout( + Duration(seconds: 2)); // Shorter duration for JournalGuy + + messageDisplayed = true; + Future.delayed(Duration(seconds: 2), () { + messageDisplayed = false; + }); + } + + @override + void updateMovement(double dt) { + if (movingLeft) { + velocity.x = -moveSpeed; + if (scale.x > 0) flipHorizontallyAroundCenter(); + } else { + velocity.x = moveSpeed; + if (scale.x < 0) flipHorizontallyAroundCenter(); + } + + if (Random().nextDouble() < 0.02) { + movingLeft = !movingLeft; + velocity = Vector2.zero(); + } + } +} diff --git a/lib/components/dialogue.dart b/lib/components/dialogue.dart new file mode 100644 index 0000000..79c33f5 --- /dev/null +++ b/lib/components/dialogue.dart @@ -0,0 +1,73 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +class NPCDialogueComponent extends PositionComponent with HasGameRef { + final String message; + final Color npcColor; // Unique color representing the NPC + late TextComponent textComponent; + late RectangleComponent backgroundBox; + late RectangleComponent npcIndicatorBox; + + NPCDialogueComponent({ + required this.message, + required this.npcColor, + }) { + priority = 100; // Display above other components + } + + @override + Future onLoad() async { + await super.onLoad(); + + // Style the dialogue text + textComponent = TextComponent( + text: message, + textRenderer: TextPaint( + style: const TextStyle( + color: Colors.white, + fontSize: 24.0, // Increased font size + fontFamily: 'Arial', + fontWeight: FontWeight.bold, + ), + ), + ); + + // Background box for the text with additional padding + backgroundBox = RectangleComponent( + size: Vector2(textComponent.width + 70, textComponent.height + 40), + paint: Paint()..color = Colors.black.withOpacity(0.85), + ) + ..position = + Vector2(30, 10) // Position with padding for the NPC indicator + ..anchor = Anchor.topLeft; + + // NPC color indicator box + npcIndicatorBox = RectangleComponent( + size: Vector2(30, textComponent.height + 40), // Increased size + paint: Paint()..color = npcColor, + ) + ..position = Vector2(0, 10) // Positioned on the left with padding + ..anchor = Anchor.topLeft; + + // Add components to the dialogue component + add(npcIndicatorBox); + add(backgroundBox); + add(textComponent); + + // Position the dialogue component at the bottom of the screen + position = Vector2( + gameRef.size.x / 2 - backgroundBox.width / 2, + gameRef.size.y - backgroundBox.height - 80, // Move it up a bit + ); + + // Center the text inside the background box + textComponent.position = backgroundBox.size / 2 - textComponent.size / 2; + } + + void showWithTimeout(Duration duration) { + // Show the dialogue and remove it after the specified timeout + Future.delayed(duration, () { + removeFromParent(); + }); + } +} diff --git a/lib/components/journalGuy.dart b/lib/components/journalGuy.dart new file mode 100644 index 0000000..2cf279f --- /dev/null +++ b/lib/components/journalGuy.dart @@ -0,0 +1,61 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:moonshiner_game/components/dialogue.dart'; +import 'package:moonshiner_game/components/npc.dart'; + +class JournalGuy extends AbstractNPC { + JournalGuy({required Vector2 position}) + : super( + npcCharacter: 'Journal Guy', + dialogues: [ + "News of the day! Get your news here!", + "Rumor has it, strange things are happening.", + "Can’t keep secrets in this town.", + ], + position: position, + ); + + @override + Color getColorForNPC() => Colors.blue; + + @override + void showDialogue() { + if (messageDisplayed) return; + + final message = dialogues[currentDialogueIndex]; + currentDialogueIndex = (currentDialogueIndex + 1) % dialogues.length; + + final npcDialogue = NPCDialogueComponent( + message: message, + npcColor: getColorForNPC(), + ); + + gameRef.add(npcDialogue); + npcDialogue.showWithTimeout( + Duration(seconds: 2)); // Shorter duration for JournalGuy + + messageDisplayed = true; + Future.delayed(Duration(seconds: 2), () { + messageDisplayed = false; + }); + } + + @override + void updateMovement(double dt) { + if (movingLeft) { + velocity.x = moveSpeed * 1.5; + if (scale.x < 0) flipHorizontallyAroundCenter(); + } else { + velocity.x = -(moveSpeed * 1.5); + if (scale.x > 0) flipHorizontallyAroundCenter(); + } + + if (Random().nextDouble() < 0.05) { + movingLeft = !movingLeft; + velocity = Vector2.zero(); + } + } +} diff --git a/lib/components/level.dart b/lib/components/level.dart index 0f5ceb2..6232f81 100644 --- a/lib/components/level.dart +++ b/lib/components/level.dart @@ -3,8 +3,12 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_tiled/flame_tiled.dart'; import 'package:moonshiner_game/components/background_tile.dart'; +import 'package:moonshiner_game/components/baker.dart'; import 'package:moonshiner_game/components/collision_block.dart'; +import 'package:moonshiner_game/components/journalGuy.dart'; +import 'package:moonshiner_game/components/oldLady.dart'; import 'package:moonshiner_game/components/player.dart'; +import 'package:moonshiner_game/components/priest.dart'; import 'package:moonshiner_game/moonshiner.dart'; import 'npc.dart'; import 'door.dart'; @@ -15,7 +19,7 @@ import 'wife.dart'; class Level extends World with HasGameRef { final String levelName; final Player player; - List npcs = []; + List npcs = []; late TiledComponent level; List collisionBlocks = []; @@ -97,55 +101,20 @@ class Level extends World with HasGameRef { add(wife); break; case 'NPC': - NPC npc; + AbstractNPC npc; final npcType = spawnPoint.name; + // Instantiate the specific NPC based on the name if (npcType == 'Priest') { - npc = NPC( - npcCharacter: 'Priest', - dialogues: [ - "The Lord sees all.", - "Bless you, my child.", - "Evil lurks in strange places." - ], - position: position, - ); + npc = Priest(position: position); } else if (npcType == 'Baker') { - npc = NPC( - npcCharacter: 'Baker', - dialogues: [ - "Fresh bread today!", - "A hard day’s work, but worth it.", - "Care for a slice?" - ], - position: position, - ); + npc = Baker(position: position); } else if (npcType == 'Old Lady') { - npc = NPC( - npcCharacter: 'Old Lady', - dialogues: [ - "In my day, things were different.", - "I’ve lived here my whole life.", - "Be careful, dear." - ], - position: position, - ); + npc = OldLady(position: position); } else if (npcType == 'Journal Guy') { - npc = NPC( - npcCharacter: 'Journal Guy', - dialogues: [ - "News of the day! Get your news here!", - "Rumor has it, strange things are happening.", - "Can’t keep secrets in this town." - ], - position: position, - ); + npc = JournalGuy(position: position); } else { - npc = NPC( - npcCharacter: 'Wanderer', - dialogues: ["I'm just here, wandering around."], - position: position, - ); + npc = Baker(position: position); // Default NPC } npc.collisionBlocks = collisionBlocks; diff --git a/lib/components/npc.dart b/lib/components/npc.dart index 7a78549..f498dc1 100644 --- a/lib/components/npc.dart +++ b/lib/components/npc.dart @@ -2,94 +2,73 @@ import 'dart:async'; import 'dart:math'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:moonshiner_game/components/collision_block.dart'; import 'package:moonshiner_game/components/custom_hitbox.dart'; +import 'package:moonshiner_game/components/dialogue.dart'; import 'package:moonshiner_game/components/player.dart'; import 'package:moonshiner_game/components/utils.dart'; import 'package:moonshiner_game/moonshiner.dart'; -import 'collision_block.dart'; -import 'hud.dart'; enum NPCState { idle, walking } -class NPC extends SpriteAnimationGroupComponent +abstract class AbstractNPC extends SpriteAnimationGroupComponent with HasGameRef, CollisionCallbacks { final String npcCharacter; - final List dialogues; // Unique dialogues for each NPC type + final List dialogues; double moveSpeed = 50; Vector2 velocity = Vector2.zero(); - Vector2 initialPosition; - CustomHitbox hitbox = CustomHitbox( - offsetX: 10, - offsetY: 4, - height: 28, - width: 14, - ); - - bool playerColliding = false; - bool playerHasInteracted = false; + bool movingLeft = true; bool messageDisplayed = false; - late final SpriteAnimation idleAnimation; - late final SpriteAnimation walkingAnimation; List collisionBlocks = []; - - bool movingLeft = true; bool movingUp = true; double changeDirectionProbability = 0.003; int stillnessCounter = 0; int maxStillnessDuration = 60; + int currentDialogueIndex = 0; - // Dialogue display message - HUDMessage hudMessage = HUDMessage( - message: "", - position: Vector2(100, 100), + CustomHitbox hitbox = CustomHitbox( + offsetX: 10, + offsetY: 4, + height: 28, + width: 14, ); - Timer? randomDialogueTimer; - - NPC({ + AbstractNPC({ required this.npcCharacter, required this.dialogues, - position, - size, - }) : initialPosition = position ?? Vector2.zero(), - super(position: position); + required Vector2 position, + int priority = 50, // Set default priority here + }) : super(position: position, priority: priority); @override - FutureOr onLoad() { - priority = 2; + FutureOr onLoad() async { _loadAllAnimations(); - add(RectangleHitbox( - position: Vector2(hitbox.offsetX, hitbox.offsetY), - size: Vector2(hitbox.width, hitbox.height), - )..debugMode = false); - - randomDialogueTimer = Timer( - Random().nextInt(6) + 5, - onTick: _triggerRandomDialogue, - repeat: true, - )..start(); - - return super.onLoad(); - } - - @override - void onRemove() { - randomDialogueTimer?.stop(); - super.onRemove(); + add(RectangleHitbox()..debugMode = false); // Basic collision hitbox + super.onLoad(); } void _loadAllAnimations() { - idleAnimation = _spriteAnimation('Idle', 11); - walkingAnimation = _spriteAnimation('Run', 12); + final idleAnimation = _spriteAnimation('Idle', 11); + final walkingAnimation = _spriteAnimation('Run', 12); animations = { NPCState.idle: idleAnimation, NPCState.walking: walkingAnimation, }; - current = NPCState.walking; } + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + if (other is Player) { + print("AbstractNPC collided with Player"); + collidingWithPlayer(); // Call your interaction logic here + } + super.onCollisionStart(intersectionPoints, other); + } + SpriteAnimation _spriteAnimation(String state, int amount) { return SpriteAnimation.fromFrameData( game.images.fromCache('Main Characters/Journal Guy/$state (32x32).png'), @@ -101,192 +80,33 @@ class NPC extends SpriteAnimationGroupComponent ); } - @override - void update(double dt) { - super.update(dt); - randomDialogueTimer?.update(dt); - - final Vector2 currentPlayerPosition = gameRef.player.position; - final double distanceToPlayer = currentPlayerPosition.distanceTo(position); - const double stoppingDistance = 50.0; - - if (distanceToPlayer < stoppingDistance) { - velocity = Vector2.zero(); - current = NPCState.idle; - } else { - _updateNPCMovement(dt); - current = NPCState.walking; - } - - _checkCollisions(); - } - - void _triggerRandomDialogue() { - if (!gameRef.currentlySpeakingNPC && - !playerColliding && - !messageDisplayed) { - hudMessage.message = dialogues[Random().nextInt(dialogues.length)]; - hudMessage.position = position + Vector2(0, -30); - - gameRef.add(hudMessage); - gameRef.currentlySpeakingNPC = true; // Set flag to indicate speaking - messageDisplayed = true; - - Future.delayed(Duration(seconds: 3), () { - if (messageDisplayed) { - gameRef.remove(hudMessage); - gameRef.currentlySpeakingNPC = false; // Reset flag after speaking - messageDisplayed = false; - } - }); + void collidingWithPlayer() { + if (!messageDisplayed && !gameRef.currentlySpeakingNPC) { + showDialogue(); } } - void stopDialogueTimer() { - randomDialogueTimer?.stop(); - } - - @override - void onCollision(Set intersectionPoints, PositionComponent other) { - if (other is Player) { - playerColliding = true; - - if (other.hasInteracted && !gameRef.currentlySpeakingNPC) { - playerHasInteracted = true; - - hudMessage.message = dialogues[Random().nextInt(dialogues.length)]; - hudMessage.position = position + Vector2(0, -30); + void showDialogue(); // Define specific behavior in each subclass - if (messageDisplayed) { - gameRef.remove(hudMessage); - } - - gameRef.add(hudMessage); - gameRef.currentlySpeakingNPC = true; // Set flag to indicate speaking - messageDisplayed = true; - - Future.delayed(Duration(seconds: 3), () { - // Check if hudMessage is still a child of the game before removing it - if (gameRef.children.contains(hudMessage)) { - gameRef.remove(hudMessage); - } - gameRef.currentlySpeakingNPC = false; // Reset flag after speaking - messageDisplayed = false; - }); - other.hasInteracted = false; - } - } - super.onCollision(intersectionPoints, other); - } + Color getColorForNPC(); // Define in each subclass @override - void onCollisionEnd(PositionComponent other) { - if (other is Player) { - playerColliding = false; - if (messageDisplayed) { - gameRef.remove(hudMessage); - gameRef.currentlySpeakingNPC = false; // Reset flag after player leaves - messageDisplayed = false; - } - current = NPCState.walking; - } - super.onCollisionEnd(other); - } - - void _updateNPCMovement(double dt) { - switch (npcCharacter) { - case 'Baker': - _bakerMovement(dt); - break; - case 'Priest': - _priestMovement(dt); - break; - case 'Journal Guy': - _journalGuyMovement(dt); - break; - case 'Old Lady': - _oldLadyMovement(dt); - break; - default: - _bakerMovement(dt); - break; - } - } - - // Movement methods for NPC types - - void _bakerMovement(double dt) { - if (movingLeft) { - velocity.x = -moveSpeed; - if (scale.x > 0) flipHorizontallyAroundCenter(); - } else { - velocity.x = moveSpeed; - if (scale.x < 0) flipHorizontallyAroundCenter(); - } - - position.x += velocity.x * dt; - - if (Random().nextDouble() < 0.02) { - movingLeft = !movingLeft; - velocity = Vector2.zero(); - } - } - - void _priestMovement(double dt) { - if (movingLeft) { - velocity.x = -(moveSpeed * 0.5); - if (scale.x > 0) flipHorizontallyAroundCenter(); - } else { - velocity.x = moveSpeed * 0.5; - if (scale.x < 0) flipHorizontallyAroundCenter(); - } - - position.x += velocity.x * dt; - - if (stillnessCounter <= 0) { - stillnessCounter = Random().nextInt(180) + 120; - velocity = Vector2.zero(); - movingLeft = !movingLeft; - } else { - stillnessCounter--; - } - } - - void _journalGuyMovement(double dt) { - if (movingLeft) { - velocity.x = moveSpeed * 1.5; - if (scale.x < 0) flipHorizontallyAroundCenter(); - } else { - velocity.x = -(moveSpeed * 1.5); - if (scale.x > 0) flipHorizontallyAroundCenter(); - } + void update(double dt) { + final Vector2 playerPosition = gameRef.player.position; + final double distanceToPlayer = playerPosition.distanceTo(position); - position.x += velocity.x * dt; + _checkCollisions(); - if (Random().nextDouble() < 0.05) { - movingLeft = !movingLeft; + if (distanceToPlayer < 50) { velocity = Vector2.zero(); - } - } - - void _oldLadyMovement(double dt) { - if (movingLeft) { - velocity.x = moveSpeed * 0.3; - if (scale.x > 0) flipHorizontallyAroundCenter(); + current = NPCState.idle; } else { - velocity.x = -moveSpeed * 0.3; - if (scale.x < 0) flipHorizontallyAroundCenter(); + updateMovement(dt); + current = NPCState.walking; } - position.x += velocity.x * dt; - - if (stillnessCounter <= 0) { - stillnessCounter = Random().nextInt(100) + 80; - velocity = Vector2.zero(); - movingLeft = !movingLeft; - } else { - stillnessCounter--; - } + position += velocity * dt; + super.update(dt); } void _checkCollisions() { @@ -326,12 +146,5 @@ class NPC extends SpriteAnimationGroupComponent } } - void collidingWithPlayer() { - velocity.x = 0; - if (velocity.x < 0) { - position.x = position.x + width + hitbox.offsetX; - } else if (velocity.x > 0) { - position.x = position.x - hitbox.offsetX - hitbox.width; - } - } + void updateMovement(double dt); } diff --git a/lib/components/oldLady.dart b/lib/components/oldLady.dart new file mode 100644 index 0000000..f3e3585 --- /dev/null +++ b/lib/components/oldLady.dart @@ -0,0 +1,61 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:moonshiner_game/components/dialogue.dart'; +import 'package:moonshiner_game/components/npc.dart'; + +class OldLady extends AbstractNPC { + OldLady({required Vector2 position}) + : super( + npcCharacter: 'Old Lady', + dialogues: [ + "In my day, things were different.", + "I’ve lived here my whole life.", + "Be careful, dear.", + ], + position: position, + ); + + @override + Color getColorForNPC() => Colors.grey; + + @override + void showDialogue() { + if (messageDisplayed) return; + + final message = dialogues[currentDialogueIndex]; + currentDialogueIndex = (currentDialogueIndex + 1) % dialogues.length; + + final npcDialogue = NPCDialogueComponent( + message: message, + npcColor: getColorForNPC(), + ); + + gameRef.add(npcDialogue); + npcDialogue.showWithTimeout( + Duration(seconds: 2)); // Shorter duration for JournalGuy + + messageDisplayed = true; + Future.delayed(Duration(seconds: 2), () { + messageDisplayed = false; + }); + } + + @override + void updateMovement(double dt) { + if (movingLeft) { + velocity.x = moveSpeed * 0.3; + if (scale.x > 0) flipHorizontallyAroundCenter(); + } else { + velocity.x = -moveSpeed * 0.3; + if (scale.x < 0) flipHorizontallyAroundCenter(); + } + + if (Random().nextDouble() < 0.02) { + movingLeft = !movingLeft; + velocity = Vector2.zero(); + } + } +} diff --git a/lib/components/player.dart b/lib/components/player.dart index 456b93b..b0435fd 100644 --- a/lib/components/player.dart +++ b/lib/components/player.dart @@ -58,7 +58,8 @@ class Player extends SpriteAnimationGroupComponent } void interact() { - hasInteracted = true; + hasInteracted = true; // This sets up interaction + print("Player interaction activated."); } @override @@ -107,8 +108,9 @@ class Player extends SpriteAnimationGroupComponent @override void onCollisionStart( Set intersectionPoints, PositionComponent other) { - if (other is ItemTip) other.collidingWithPlayer(); - if (other is NPC && hasInteracted) other.collidingWithPlayer(); + if (other is ItemTip) { + other.collidingWithPlayer(); + } super.onCollisionStart(intersectionPoints, other); } diff --git a/lib/components/priest.dart b/lib/components/priest.dart new file mode 100644 index 0000000..e9aa230 --- /dev/null +++ b/lib/components/priest.dart @@ -0,0 +1,61 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:moonshiner_game/components/dialogue.dart'; +import 'package:moonshiner_game/components/npc.dart'; + +class Priest extends AbstractNPC { + Priest({required Vector2 position}) + : super( + npcCharacter: 'Priest', + dialogues: [ + "The Lord sees all.", + "Bless you, my child.", + "Evil lurks in strange places.", + ], + position: position, + ); + + @override + Color getColorForNPC() => Colors.purple; + + @override + void showDialogue() { + if (messageDisplayed) return; + + final message = dialogues[currentDialogueIndex]; + currentDialogueIndex = (currentDialogueIndex + 1) % dialogues.length; + + final npcDialogue = NPCDialogueComponent( + message: message, + npcColor: getColorForNPC(), + ); + + gameRef.add(npcDialogue); + npcDialogue.showWithTimeout( + Duration(seconds: 2)); // Shorter duration for JournalGuy + + messageDisplayed = true; + Future.delayed(Duration(seconds: 2), () { + messageDisplayed = false; + }); + } + + @override + void updateMovement(double dt) { + if (movingLeft) { + velocity.x = -moveSpeed * 0.5; + if (scale.x > 0) flipHorizontallyAroundCenter(); + } else { + velocity.x = moveSpeed * 0.5; + if (scale.x < 0) flipHorizontallyAroundCenter(); + } + + if (Random().nextDouble() < 0.02) { + movingLeft = !movingLeft; + velocity = Vector2.zero(); + } + } +} diff --git a/lib/moonshiner.dart b/lib/moonshiner.dart index c8daddc..fa89a05 100644 --- a/lib/moonshiner.dart +++ b/lib/moonshiner.dart @@ -23,7 +23,7 @@ class Moonshiner extends FlameGame // UI Components late JoystickComponent joyStick; late Button interactButton; - List activeNpcs = []; // Track active NPCs in the current level + List activeNpcs = []; // Track active NPCs in the current level late HUDMessage hudMessage; bool hasShownLevelOneIntro =