Files
15112-fishing-miniproject/main.py
T

723 lines
30 KiB
Python

# =============================================================================
# Fishing Mini-Project - Main Game File (OUTLINE ONLY)
# =============================================================================
# This file is a STRUCTURAL OUTLINE: function signatures + descriptive comments.
# The actual implementation will be written by the project author (human).
#
# Engine: cmu_graphics | Pattern: MVC (Model-View-Controller)
# Data: creatures.tsv (tab-separated database at repo root)
# Save: save.json (persistent progress in the script directory)
# =============================================================================
from cmu_graphics import *
import json
import random
import math
import os
#debug vars to store mouse pos
gmouseX = 0
gmouseY = 0
# =============================================================================
# SECTION 1: DATA LOADING / FILE I/O
# -----------------------------------------------------------------------------
# Helpers that read from and write to disk. Kept separate from game logic so
# the data source (TSV path, save path) can change without touching gameplay.
# =============================================================================
def loadCreatures(path="creatures.tsv"):
# Read creatures.tsv (tab-separated) and parse it into a usable structure.
# Suggested return: a list of dicts, one per creature, with keys matching
# the header row: name, description, price, sprite-file, difficulty,
# length, weight. Convert numeric columns (price, difficulty, length,
# weight) from strings to int/float here; leave missing fields as sensible
# defaults so partially-filled rows (like the current TSV) don't crash.
creatures = []
with open(path) as f:
lines = f.readlines()
for line in lines[1:]: #ignore header
data = line.split("\t")
creatures.append({
"name": data[0],
"description": data[1],
"price": float(data[2]),
"sprite-file": data[3],
"difficulty": int(data[4]),
"length": float(data[5]), #inches
"weight": float(data[6]) #pounds
})
return creatures
def loadSave(path="save.json"):
# Load save.json and return the player's saved state (money, owned baits,
# owned upgrades, current bait, caught log, etc.). If the file is missing
# or unreadable, return a fresh default-state dict so a first-time player
# starts cleanly. This is the only place that reads the save file.
defaultState = {
"cash": 0,
"owned_baits": ["Worms"],
"owned_upgrades": [],
"current_bait": "Worms",
"catch_log": []
}
try:
with open(path, "r") as f:
return json.load(f)
except (FileNotFoundError, IOError):
return defaultState
def writeSave(saveState, path="save.json"):
# Serialize the current player state to save.json. Call this at key moments
# (after a sale, after a purchase, on quit) so progress persists between
# sessions. Keep the written structure identical to what loadSave expects.
with open(path, "w") as f: #overwrite existing
f.write(json.dumps(saveState))
# =============================================================================
# SECTION 2: MODEL - STATE SETUP & GAME DATA
# -----------------------------------------------------------------------------
# Everything that initializes and holds game state on the cmu_graphics `app`.
# No drawing and no event handling here - just state.
# =============================================================================
AVAILABLE_UPGRADES = [ # (name, bonus_type, bonus_modifier, price)
#power mods - lowers speed of cursor (additive)
("Smart Reel", "power", 2, 100),
("Clever Reel", "power", 7, 350),
("Intelligent Reel", "power", 10, 1500),
("Genius Reel", "power", 20, 20000),
#timing mods - lengthens bars in skill check (multiplicative)
("Medium Bobber", "timing", 1.1, 100),
("Large Bobber", "timing", 1.2, 300),
("Huge Bobber", "timing", 1.5, 1600),
("Massive Bobber", "timing", 2, 25000),
#count mods - lowers number of bars in skill check (subtractive (capped at 1))
("Shiny Rod", "count", 1, 450),
("Spectral Rod", "count", 1, 1200),
("Glowing Rod", "count", 2, 5000),
("Galaxy Rod", "count", 3, 40000)
]
AVAILABLE_BAITS = [ # (name, max_difficulty, price, image_file)
("Worms", 30, 0, "worm.png"),
("Prawns", 50, 200, "prawn.png"),
("Tuna", 70, 1000, "tuna.png"),
("Bass", 100, 19410, "bass.png")
]
def onAppStart(app):
# cmu_graphics entry point. Initialize ALL app.* state here:
# - window/layout constants (width, height, dock/water/grass regions)
# - app.creatures (from loadCreatures)
# - app.save (from loadSave) and derived player values
# - fishing state (idle / waiting-for-bite / skill-check / result)
# - skill-check state placeholders (cursor angle, bars, speed, result)
# - app.stepsPerSecond for the game loop cadence
# Delegates to the smaller setup helpers below to keep this readable.
app.debug = True
#DRAWING CONSTANTS
app.width = 600
app.height = 400
app.background = 'lightSkyBlue'
app.waterColor = 'midnightBlue'
app.grassColor = 'seaGreen'
app.waterHeight = app.height // 3
app.waterTop = app.height - app.waterHeight
app.grassHeight = 20
app.grassTop = app.height - app.waterHeight - app.grassHeight
#GAME STATE/SETUP
app.stepsPerSecond = 30
app.creatures = loadCreatures()
app.save = loadSave()
app.scene = "idle" # idle, waiting-for-bite, skill-check, result, shopping, menu
#SKILL CHECK
app.skillCheckAngle = 0
app.skillCheckSpeed = 1
app.skillCheckBars = [] # tuples of (position, length)
app.skillCheckFailed = False
#FISHING
app.fishingTimer = 0
app.hookedFish = None
app.timeSinceInit = 0
#list of interactables with name and their positions
#(name, sceneFilter, left, right, top, bottom)
app.INTERACTABLES = [
("open-shop", "idle", 400, 540, app.grassTop - 130, app.grassTop + 30),
("close-shop", "shop", app.width-50, app.width, 0, 50)
]
#add all of our shop purchase upgrades
startX = 75
startY = 75
endX = 525
endY = 250
colInd = 0
XDiff = (endX-startX) // 3
#build upgrade grid
for columnType in ["power", "timing", "count"]:
upgrades = [u for u in AVAILABLE_UPGRADES if u[1] == columnType]
YDiff = (endY-startY) // len(upgrades)
for i in range(len(upgrades)):
app.INTERACTABLES.insert(0, ("upgrade-buy:" + upgrades[i][0],
"shop",
startX + XDiff * colInd,
startX + XDiff * colInd + XDiff,
startY + YDiff * i,
startY + YDiff * i + YDiff)) #this used to just be append
#but that was causing issues where the shop bounding box was
#checked first, then opening the shop gui, and allowing the
#player to purchase an upgrade with the same click (not ideal)
colInd += 1
#build bait grid
startX = 125
startY = 250
endX = 475
diffX = (endX - startX) // len(AVAILABLE_BAITS)
for i in range(len(AVAILABLE_BAITS)):
app.INTERACTABLES.insert(0, ("bait-buy:" + AVAILABLE_BAITS[i][0],
"shop",
startX + diffX * i,
startX + diffX * i + diffX,
startY,
startY + diffX))
print(app.INTERACTABLES)
def resetFishingState(app):
# Return the fishing cycle to its starting "idle" point so a new cast can
# begin. Clears any active skill-check data, the pending random bite timer,
# and the currently-hooked creature. Called at startup and after each catch
# is resolved/dismissed.
app.skillCheckAngle = 0 #measured in degrees
app.skillCheckSpeed = 1
app.skillCheckBars = [] # list of tuples of form (position, length)
app.skillCheckFailed = False
app.hookedFish = None
app.fishingTimer = 0
app.scene = "idle"
pass
def pickRandomCreature(app):
# Choose which creature the player hooks on this cast. Selection is filtered
# by the current bait (bait gates which difficulty tiers are available) and
# weighted toward easier creatures, while still giving rarer/harder ones a
# real chance (per the spec's "skewed toward easier but still fun" goal).
# Returns the chosen creature record (one entry from app.creatures).
max_difficulty = [bait[1] for bait in AVAILABLE_BAITS if bait[0] == app.save["current_bait"]][0]
#here we take the root of the difficulty to represent the weight of that fish, this makes higher difficulty fish easier to catch once unlocked
k = 0.5
#invert the difficulty and add one so it avoids zero
possibilities = [(creature["name"], (max_difficulty - creature["difficulty"] + 1) ** k) for creature in app.creatures if creature["difficulty"] <= max_difficulty]
total = sum([c[1] for c in possibilities])
#now pick randomly
roll = random.random() * (total) # a value between 0 and total
caughtName = "Aubrey Fish"
for possibleCatch in possibilities:
roll -= possibleCatch[1]
if (roll < 0):
caughtName = possibleCatch[0]
break
print(caughtName, app.creatures)
catch = [c for c in app.creatures if c["name"] == caughtName][0]
return catch
def startSkillCheck(app, creature):
# Configure a new skill check based on the hooked creature's difficulty.
# Difficulty maps to: cursor rotation speed, number of target bars, and
# bar length/width. Also apply any owned upgrades that modify these values
# (e.g. slower cursor, wider bars, extra forgiveness). Stores the resulting
# parameters on app for the controller/view to use.
app.scene = "skill-check"
difficulty = creature["difficulty"]
fishingPower = 1 + sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "power")])
app.skillCheckSpeed = max(difficulty / fishingPower, 5) #this got INCREDIBLY slow with good upgrades
numBars = difficulty // 10 - sum([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "count")])
numBars = max(numBars, 1)
lastPos = 0
lastLen = 0
while len(app.skillCheckBars) < numBars:
#50 is default length, adjust it proportionally to difficulty, then add upgrade length multipliers. also set to int instead of float
newLen = random.randrange(int((1 - (difficulty / 100)) * 50 * math.prod([upgrade[2] for upgrade in AVAILABLE_UPGRADES if (upgrade[0] in app.save["owned_upgrades"] and upgrade[1] == "timing")])))
#take the end of the last bar, and add a random amount between 5 degrees
#and the remaining amount to the end divided by the number of bars left
lastEndPos = (lastPos + lastLen)
newPos = lastEndPos + random.randrange(5, (360 - lastEndPos) // (numBars - len(app.skillCheckBars)))
#cap it to ensure we dont go over
newPos = min(newPos, 360-newLen)
#TODO: double check that multiple bars do not overlap
app.skillCheckBars.append((newPos, newLen))
lastPos = newPos
lastLen = newLen
#speed is set, bars are set, scene is good, should be fine?
def beginFishing(app):
app.fishingTimer = random.random() * app.stepsPerSecond * 25 + 5 # completely random between 5 and 30
app.scene = "waiting-for-bite"
# =============================================================================
# SECTION 3: CONTROLLER - GAME LOOP & INPUT HANDLING
# -----------------------------------------------------------------------------
# cmu_graphics event hooks. These advance state and route input; they should
# call Model helpers rather than contain heavy logic themselves.
# =============================================================================
def onStep(app):
# Runs every time step (the game loop / "controller tick").
# Responsibilities depend on the current fishing phase:
# - waiting-for-bite: count down the random timer; when it elapses, pick a
# creature and transition into the skill check.
# - skill-check: advance the cursor's angle around the circle each step.
# - otherwise (idle, viewing a catch, in shop): do nothing time-based.
app.timeSinceInit += 1
#had to look up how to do switch-case statement in python, I know it from JS though
match app.scene:
case "waiting-for-bite":
app.fishingTimer -= 1
if app.fishingTimer < 1:
selectedCreature = pickRandomCreature(app)
app.hookedFish = selectedCreature
startSkillCheck(app, selectedCreature)
case "skill-check":
app.skillCheckAngle += app.skillCheckSpeed / 6 #this was far too fast
if (app.skillCheckAngle > 360):
finishSkillCheck(app)
def onMousePress(app, mouseX, mouseY):
# Primary input router. Behavior depends on app.scene / fishing phase:
# - dock idle: clicking starts a cast (begin waiting-for-bite).
# - skill-check active: register the click as a timing attempt and
# evaluate it via checkSkillHit.
# - catch popup showing: dismiss the popup and reset fishing.
# - shop / UI sprites clicked: open the shop or trigger that UI action.
# Keep this as dispatch only - defer the real work to helper functions.
match app.scene:
case "idle" | "shop": #also had to look this up - pipe operator is dumb. just let me have multiple cases
interacted = False
#check if we clicked a hud element/interactable
for name, sceneFilter, left, right, top, bottom in app.INTERACTABLES:
if left < mouseX < right and top < mouseY < bottom \
and sceneFilter == app.scene:
interacted = True
match name:
case "open-shop":
app.scene = "shop"
case "close-shop":
app.scene = "idle"
if name.startswith("upgrade-buy:"):
print("buying upgrade", name)
buyUpgrade(app, name.split(":")[1])
if name.startswith("bait-buy:"):
print("buying bait", name)
buyBaitLicense(app, name.split(":")[1])
if not interacted and app.scene == "idle":
beginFishing(app)
case "result":
sellCreature(app, app.hookedFish)
resetFishingState(app)
case "skill-check":
checkSkillHit(app)
case "waiting-for-bite":
resetFishingState(app)
def onKeyPress(app, key):
# Optional keyboard controls. Candidates: a key to cast/reel as an
# alternative to clicking, a key to open/close the shop, or a debug/reset
# key during development. Remove if you decide the game is mouse-only.
#decidedly unused for now, i prefer mouse-only. ill keep the definition here in case i need debug tools.
# DEBUG
if not app.debug: return
if key == "m":
app.save["cash"] = 1000000
if key == "escape":
os._exit(0) #suggested by AI, instantly exits without hanging like sys.exit(0)
if key == "r":
#reset save
app.save = loadSave("thisisntarealsavefile.stupid")
#added by AI, but simply allows me to trigger a breakpoint manually to check state of app and variables
#after some debate, this was the best option over something more complex
if key == 'd':
print("breakpoint here")
def onMouseMove(app, mouseX, mouseY):
global gmouseX, gmouseY
gmouseX, gmouseY = mouseX, mouseY
def checkSkillHit(app):
# Evaluate a skill-check click: is the cursor's current angle inside one of
# the active target bars? Decide hit vs miss. Define the multi-bar flow here
# (e.g. must the player clear every bar, or is one good hit enough?) and
# advance or fail the skill check accordingly. On full success, mark the
# creature as caught and move to the catch-display phase; on failure, end
# the cast without a catch.
try:
targetPos, targetLen = app.skillCheckBars.pop(0)
except IndexError:
targetPos = targetLen = -1 #make it impossible to succeed, this means they clicked after already getting rid of all bars
if not ( 0 < (app.skillCheckAngle - targetPos) < targetLen): #check if we hit a bar
#if we succeed, nothing needs to happen, we popped the bar
#if we fail, we either didnt hit or missed a past bar, either way we can jump straight to the fail screen
app.hookedFish = None
app.scene = "result"
print("failed",targetPos,targetLen,app.skillCheckAngle)
if len(app.skillCheckBars) < 1: #QOL: dont make them wait on an empty loop
finishSkillCheck(app)
def finishSkillCheck(app):
if len(app.skillCheckBars) > 0:
app.hookedFish = None #meaning we failed
app.scene = "result"
# =============================================================================
# SECTION 4: SHOP / ECONOMY LOGIC
# -----------------------------------------------------------------------------
# Buying/selling and the rogue-like upgrade system. Pure state changes that the
# controller invokes; no drawing here.
# =============================================================================
def sellCreature(app, creature):
# Add the creature's market value to the player's money. Value scales with
# difficulty (harder creatures pay more, per the spec). Persist the updated
# money via writeSave. Decide here whether selling is automatic on catch or
# a deliberate action in the shop/market.
#selling will be automatic
if creature == None: return
app.save["cash"] += creature["price"]
writeSave(app.save)
def buyUpgrade(app, upgradeId):
# Attempt to purchase an upgrade. Check the player can afford it and doesn't
# already own it; if valid, deduct money, record ownership in app.save, and
# persist. Upgrades modify the skill check (cursor speed, bar size/count,
# extra attempts, etc.) and are read back in startSkillCheck.
upgrade = [u for u in AVAILABLE_UPGRADES if u[0] == upgradeId][0] #find the specified upgrade
name, category, value, price = upgrade
print("attempting buy",upgrade)
if name in app.save["owned_upgrades"]: return False
if price > app.save["cash"]: return False
print("success!")
app.save["owned_upgrades"].append(name)
app.save["cash"] -= price
writeSave(app.save)
def buyBaitLicense(app, baitId):
# Purchase a one-time license for a bait tier. Once owned, the player can
# freely switch to that bait at no further cost (switching handled by
# setActiveBait). Bait determines which creatures are reachable in
# pickRandomCreature. Deduct money, record ownership, persist.
bait = [b for b in AVAILABLE_BAITS if b[0] == baitId][0] #find the specified upgrade
name, max_difficulty, price, img_file = bait
print("attempting buy",bait)
if name in app.save["owned_baits"]: return False
if price > app.save["cash"]: return False
print("success!")
app.save["owned_baits"].append(name)
app.save["cash"] -= price
writeSave(app.save)
def setActiveBait(app, baitId):
# Switch the currently-equipped bait to any bait the player already owns a
# license for. Free action. Updates app state so the next cast's creature
# pool reflects the new bait. Reject the switch if the bait isn't owned.
bait = [b for b in AVAILABLE_BAITS if b[0] == baitId][0] #find the specified upgrade
name, max_difficulty, price = bait
if name in app.save["owned_baits"]:
app.save["current_bait"] = name
writeSave(app.save)
# =============================================================================
# SECTION 5: VIEW - DRAWING
# -----------------------------------------------------------------------------
# redrawAll + per-scene draw helpers. These ONLY read state and draw; they must
# never mutate app state. Composed top-down so redrawAll stays legible.
# =============================================================================
def redrawAll(app):
# Master draw routine. Draw the persistent world first (background, grass,
# water, dock, player), then layer the active scene/phase on top:
# - the shop UI, or
# - the active skill-check overlay, or
# - the catch-display popup.
# Delegates each piece to the helpers below.
drawBackground(app)
if app.scene == "waiting-for-bite":
drawBobber(app)
if app.scene == "shop":
drawShopEntities(app)
if app.scene == "skill-check":
drawSkillCheck(app)
if app.scene == "result":
drawCatchDisplay(app)
if app.debug:
drawDebug(app)
#my self-hosted gitea repo also stores the image files, very convienient
BASE_IMAGE_URL = "https://gitea.lockeshor.com/lockeshor/15112-fishing-miniproject/raw/branch/main/sprites/"
#REMINDERS:
# drawRect(left, top, width, height, fill="color")
# drawImage(url, )
# drawLabel(text, centerX, centerY)
def drawBackground(app):
# Draw the static scene layout from the spec: grass behind, the dock, and
# the lake occupying the bottom ~1/3 of the screen (blue water, optional
# wave patterns). This is the backdrop present in every scene.
#water
drawRect(0, app.waterTop, app.width, app.waterHeight, fill=app.waterColor)
#grass
drawRect(0, app.grassTop, app.width, app.grassHeight, fill=app.grassColor)
#dock
dockColor = 'sienna'
dockLegColor = 'maroon'
drawRect(0,app.grassTop - 50, 200, 40, fill = dockColor)
drawRect(40, app.grassTop - 10, 20, app.height, fill = dockLegColor)
drawRect(90, app.grassTop - 10, 20, app.height, fill = dockLegColor)
drawRect(140, app.grassTop - 10, 20, app.height, fill = dockLegColor)
#image cred: https://pngtree.com/freepng/cartoon-style-illustration-of-a-young-sleeping-men-fishing-on-a-wood-chair_15775817.html
#fisherman
drawImage(BASE_IMAGE_URL + "fisherman.png",
60, app.grassTop - 50 - 140, width=140, height=170)
#image cred: https://www.artstation.com/artwork/xzLbe2
#salesman
drawImage(BASE_IMAGE_URL + "salesman.png",
400, app.grassTop - 130, width = 140, height= 160)
#cash
drawLabel(f"${app.save["cash"]}",app.width - 5,5,align="right-top",size=26,fill='darkgoldenrod',font='orbitron')
def drawBobber(app):
# Draw the character sprite seated at the end of the dock with the fishing
# rod. When a cast is active, draw the line and the bobber dropping onto the
# water surface. Reflects the current fishing phase visually.
#cast line
drawLine(172, 64, 370, 280, fill="white")
#bobber?
def drawShopEntities(app):
# Draw the clickable shop/UI sprites standing on the grass (per the spec).
# Positions here must line up with the hit-testing done in onMousePress so
# clicks land on the right entity.
#dimming
drawRect(0,0,app.width,app.height,opacity=50)
#menu background
drawRect(50,50,app.width-100,app.height-100,fill='bisque',border='lightCoral',borderWidth=5)
#exit "X"
drawLine(app.width-45, 5, app.width-5, 45, fill='red', lineWidth=5)
drawLine(app.width-5, 5, app.width-45, 45, fill='red', lineWidth=5)
#draw upgrade purchases
for name, sceneFilter, left, right, top, bottom in [interactable for interactable in app.INTERACTABLES if interactable[0].startswith("upgrade-buy:")]:
#print("drawing",name,left,right,top,bottom)
displayName = name.split(":")[1]
#bg
drawRect(left, top, right - left, bottom - top, fill=('gray' if displayName in app.save["owned_upgrades"] else 'rosyBrown'), border='bisque',borderWidth=4)
centerY = top+(bottom-top)//2
#name
drawLabel(displayName, left+8, centerY, align="left",size=12)
#price
price = [u for u in AVAILABLE_UPGRADES if u[0] == displayName][0][3]
#pricebg
drawRect(right-8-40, top + 10, 40, bottom-top-20, fill="gold")
drawLabel("$" + str(price), right - 10, centerY, align="right", size = 10)
#draw bait purchases
for name, sceneFilter, left, right, top, bottom in [interactable for interactable in app.INTERACTABLES if interactable[0].startswith("bait-buy:")]:
displayName = name.split(":")[1]
drawRect(left, top, right - left, bottom - top, fill=('gray' if displayName in app.save["owned_baits"] else 'rosyBrown'), border='bisque',borderWidth=4)
drawLabel(displayName, left + (right-left) / 2, bottom - 20, align="center")
img_file = [b for b in AVAILABLE_BAITS if b[0] == displayName][0][3]
#Image creds: ALL ICONS FROM https://www.flaticon.com
drawImage(BASE_IMAGE_URL + "bait-icons/" + img_file, left + (right-left) / 2, top + (bottom-top) / 2 - 10, width=80,height=80,align="center")
def drawSkillCheck(app):
# Draw the active skill-check overlay: the circle, the target bars placed
# around it (count/length from difficulty), and the rotating cursor at its
# current angle. Optionally show which bars are already cleared. Read-only.
#IMPORTANT: circle is like unit circle for drawing, but we want it to have 0 at the top. transformations will be needed
#radius of circle
r = 90
#basic path
#REMINDER: cx, cy, r
drawCircle(300, 130, r, fill=None,border='dimGray')
#bars
for barPos, barLen in app.skillCheckBars:
drawArc(300,130, r * 2 + 20, r * 2 + 20, -barPos + 90 - barLen, barLen, fill="white")
#cover up center of bars
drawCircle(300, 130, r - 10, fill=app.background)
#cursor
#REMINDER: width and height are for oval (so r * 2)
#REMINDER: cx, cy, width, height, startangle, sweepangle
drawArc(300,130, r * 2 + 20, r * 2 + 20, -app.skillCheckAngle + 90, 2, fill="red")
def drawCatchDisplay(app):
# Draw the "you caught it!" popup: the creature's sprite coming out of the
# water, plus its name, description, and stats (length, weight, value).
# Stays up until dismissed by a click (handled in onMousePress). Read-only.
#dimming
drawRect(0,0,app.width,app.height,opacity=75)
#star of surprise (turns out I cant make a star with assymetric height/width, so ill have to use an image)
#Image cred: https://pngtree.com/free-png-vectors/comics-bubbles
drawImage(BASE_IMAGE_URL + "surprise.png", app.width // 2, app.height // 2 - 40, align = "center",width = 530, height = 530)
#draw label
if app.hookedFish != None:
drawLabel(f"You Caught A {app.hookedFish["name"]}!",app.width // 2,210,align="center",size=18)
drawLabel(app.hookedFish["description"],app.width // 2,230,align="center",size=12)
#money bonus
drawRect(460, 180, 120, 45, align = "center", rotateAngle = -25, fill='mediumSpringGreen', border="black")
drawLabel(f"+ ${app.hookedFish["price"]}",460, 180, size=24, align = "center", rotateAngle = -25, fill='black')
#length and weight
drawLabel(f"{app.hookedFish["length"]} in.",app.width // 2 - 40,260,align="center",size=14)
drawLabel(f"{app.hookedFish["weight"]} lbs.",app.width // 2 + 40,260,align="center",size=14)
#draw creature/error
imgWidth = 200
if app.hookedFish != None:
trueimgW, trueimgH = getImageSize(BASE_IMAGE_URL + f"fish/{app.hookedFish["sprite-file"]}")
imgHeight = imgWidth * (trueimgH / trueimgW)
drawImage(BASE_IMAGE_URL + f"fish/{app.hookedFish["sprite-file"]}",
app.width // 2,
app.height // 2 - 75,
align = "center",
width=imgWidth,
height=imgHeight,
rotateAngle = math.sin(app.timeSinceInit / 20) * 10)
else:
#Image Cred: https://freepngimg.com/png/13474-fail-stamp-free-download-png
trueimgW, trueimgH = getImageSize(BASE_IMAGE_URL + "fail.png")
imgHeight = imgWidth * (trueimgH / trueimgW)
drawImage(BASE_IMAGE_URL + "fail.png",
app.width // 2,
app.height // 2,
align = "center",
width=imgWidth,
height=imgHeight,
rotateAngle = math.sin(app.timeSinceInit / 2) * 30 - 15)
def drawHud(app):
# Draw the persistent heads-up display: current money, equipped bait, and
# any brief status/instruction text ("click to cast", etc.). Overlaid on
# every scene so the player always sees their core stats.
#I dont really need this unless I decide to add alert/tutorial messages
pass
def drawDebug(app):
debugStr = f"""\
mX: {gmouseX}, mY: {gmouseY}
Scene: {app.scene}
fishingTimer: {rounded(app.fishingTimer)}
hookedFish: {app.hookedFish["name"] if app.hookedFish else "N/A"} @ {app.hookedFish["difficulty"] if app.hookedFish else "N/A"} difficulty
"""
debugLines = debugStr.splitlines()
size = 16
for i in range(len(debugLines)):
drawLabel(debugLines[i], 0, app.height - 125 + size * i, fill="white", size=size, align = "left")
# =============================================================================
# SECTION 6: PROGRAM ENTRY
# -----------------------------------------------------------------------------
def main():
# Start the cmu_graphics app (runApp with the chosen window dimensions).
runApp()
main()